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); } });