Files
Friendship-and-chat-api/script/migrate-db-files-to-oss/test/source-current.test.mjs
T
dzc f55561c98d feat:上传到阿里云oss
feat:把以前的图片迁移到阿里云oss
2026-11-05 15:22:55 +08:00

132 lines
6.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { sourceCurrent } from '../migrate.mjs';
const config = { mysql: { database: 'source_current_test' } };
const item = {
contentId: '9007199254740993', configId: '9007199254740995',
path: '头像/Résumé + 100%.png', size: 1234, sha256: 'a'.repeat(64)
};
const current = overrides => ({ id: item.contentId, size: String(item.size), sha256: item.sha256, ...overrides });
class FakeDb {
constructor(responses) {
this.responses = responses;
this.calls = [];
}
async execute(sql, params) {
assert.match(sql, /^SELECT\b/, 'source validation must not write or manage the caller transaction');
this.calls.push({ sql, params });
if (this.calls.length > this.responses.length) throw new Error('Unexpected extra database query');
const response = this.responses[this.calls.length - 1];
if (response instanceof Error) throw response;
return [response];
}
}
function assertMetadataLookup(call, expectedItem, locked = false) {
assert.match(call.sql, /^SELECT id FROM /);
assert.doesNotMatch(call.sql, /\bcontent\b|SHA2\s*\(|OCTET_LENGTH\s*\(/i,
'the unindexed path/order lookup must not project file bytes or content expressions');
assert.match(call.sql, /WHERE deleted=0 AND config_id=\? AND path=\? ORDER BY id DESC LIMIT 1/);
assert.doesNotMatch(call.sql, /BINARY\s+path|LOWER\s*\(path\)/i, 'preserve native MySQL path collation');
assert.deepEqual(call.params, [expectedItem.configId, expectedItem.path]);
if (locked) assert.match(call.sql, / FOR UPDATE$/);
else assert.doesNotMatch(call.sql, /FOR UPDATE/i);
}
function assertPrimaryKeyHashRead(call, expectedItem, locked = false) {
assert.match(call.sql, /OCTET_LENGTH\(content\) AS size/);
assert.match(call.sql, /LOWER\(SHA2\(content,256\)\) AS sha256/);
assert.match(call.sql, /WHERE id=\? AND config_id=\? AND path=\? AND deleted=0/);
assert.doesNotMatch(call.sql, /ORDER BY|LIMIT|JOIN/i, 'read the digest through a single primary-key lookup');
assert.deepEqual(call.params, [expectedItem.contentId, expectedItem.configId, expectedItem.path]);
if (locked) assert.match(call.sql, / FOR UPDATE$/);
else assert.doesNotMatch(call.sql, /FOR UPDATE/i);
}
test('looks up the latest ID without BLOB expressions, then validates size and SHA by primary key', async () => {
const db = new FakeDb([[{ id: item.contentId }], [current()]]);
await sourceCurrent(db, config, item);
assert.equal(db.calls.length, 2);
assertMetadataLookup(db.calls[0], item);
assertPrimaryKeyHashRead(db.calls[1], item);
});
test('rejects a newer active version before any content/hash query, without rounding bigint IDs', async () => {
const db = new FakeDb([[{ id: '9007199254740994' }]]);
await assert.rejects(sourceCurrent(db, config, item), /源图片.*变化或已删除/);
assert.equal(db.calls.length, 1);
assertMetadataLookup(db.calls[0], item);
});
test('rejects a missing latest record without issuing a content/hash query', async () => {
const db = new FakeDb([[]]);
await assert.rejects(sourceCurrent(db, config, item), /源图片.*变化或已删除/);
assert.equal(db.calls.length, 1);
assertMetadataLookup(db.calls[0], item);
});
test('rejects content disappearing between the metadata and primary-key reads', async () => {
const db = new FakeDb([[{ id: item.contentId }], []]);
await assert.rejects(sourceCurrent(db, config, item), /源图片.*变化或已删除/);
assert.equal(db.calls.length, 2);
assertPrimaryKeyHashRead(db.calls[1], item);
});
test('rejects changed length, changed hash, or an unexpected primary-key row', async t => {
for (const [name, row] of [
['changed size', current({ size: item.size + 1 })],
['changed hash with the same size', current({ sha256: 'b'.repeat(64) })],
['wrong content ID', current({ id: '9007199254740994' })],
['missing hash', current({ sha256: null })]
]) {
await t.test(name, async () => {
const db = new FakeDb([[{ id: item.contentId }], [row]]);
await assert.rejects(sourceCurrent(db, config, item), /源图片.*变化或已删除/);
assert.equal(db.calls.length, 2);
assertPrimaryKeyHashRead(db.calls[1], item);
});
}
});
test('retains FOR UPDATE on both validation queries when the caller owns a transaction', async () => {
const db = new FakeDb([[{ id: item.contentId }], [current()]]);
await sourceCurrent(db, config, item, true);
assert.equal(db.calls.length, 2);
assertMetadataLookup(db.calls[0], item, true);
assertPrimaryKeyHashRead(db.calls[1], item, true);
});
test('locked validation still stops at the metadata lookup if the latest version changed', async () => {
const db = new FakeDb([[{ id: '9007199254740994' }]]);
await assert.rejects(sourceCurrent(db, config, item, true), /源图片.*变化或已删除/);
assert.equal(db.calls.length, 1);
assertMetadataLookup(db.calls[0], item, true);
});
test('supports unsigned-bigint-sized IDs and binds paths without interpolation or normalization', async () => {
const largeItem = { ...item, contentId: '18446744073709551615', path: "case/IMAGE.png' OR 'x'='x" };
const db = new FakeDb([[{ id: largeItem.contentId }], [current({ id: largeItem.contentId })]]);
await sourceCurrent(db, { mysql: { database: 'app`db' } }, largeItem, false);
assertMetadataLookup(db.calls[0], largeItem);
assertPrimaryKeyHashRead(db.calls[1], largeItem);
for (const call of db.calls) {
assert.match(call.sql, /`app``db`\.`infra_file_content`/);
assert.ok(!call.sql.includes(largeItem.contentId));
assert.ok(!call.sql.includes(largeItem.path));
}
});
test('propagates database failures instead of accepting an unverified source', async () => {
for (const responses of [
[Object.assign(new Error('metadata lookup timed out'), { code: 'TEST_TIMEOUT' })],
[[{ id: item.contentId }], Object.assign(new Error('hash read failed'), { code: 'TEST_READ_FAILURE' })]
]) {
const db = new FakeDb(responses);
const expected = responses[responses.length - 1];
await assert.rejects(sourceCurrent(db, config, item), error => error === expected);
assert.equal(db.calls.length, responses.length);
}
});