import test from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { git, migrationManifest } from '../lib.mjs'; function fixture(t) { const cwd = mkdtempSync(join(tmpdir(), 'fedeo-migrations-test-')); t.after(() => rmSync(cwd, { recursive: true, force: true })); const run = args => git(args, cwd, { stdio: ['pipe', 'pipe', 'pipe'] }); run(['init', '-b', 'main']); run(['config', 'user.name', 'Release Test']); run(['config', 'user.email', 'release@example.test']); // Keep CRLF test bytes in Git regardless of the developer's autocrlf setting. writeFileSync(join(cwd, '.gitattributes'), '*.sql -text\n'); const write = (path, data) => { mkdirSync(dirname(join(cwd, path)), { recursive: true }); writeFileSync(join(cwd, path), data); }; const commit = () => { run(['add', '.']); run(['commit', '--allow-empty', '-m', 'test: add migrations']); return run(['rev-parse', 'HEAD']); }; return { cwd, run, write, commit }; } test('large migration hashes use the complete committed bytes beyond process buffer limits', async t => { const f = fixture(t); const path = 'backend/db/migrations/0058_reference_data.sql'; f.write(path, ''); const expected = createHash('sha256'); const chunk = Buffer.alloc(1024 * 1024, Buffer.from(" INSERT INTO reference VALUES ('Grüße');\r\n")); // Exceed both Node's 1 MiB default and the text Git helper's 32 MiB buffer. for (let i = 0; i < 34; i++) { appendFileSync(join(f.cwd, path), chunk); expected.update(chunk); } const sha = f.commit(); f.write(path, 'local changes must not affect the release checksum'); assert.deepEqual(await migrationManifest(sha, f.cwd), [{ path, sha256: expected.digest('hex') }]); }); test('both migration folders are hashed byte-for-byte and non-SQL files are excluded', async t => { const f = fixture(t); const files = [ ['backend/db/migrations/0000_init.sql', Buffer.from(' \r\nSELECT 1;\r\n\t')], ['push-server/packages/db/drizzle/0000_init.sql', Buffer.from('SELECT \'äöü\';\n')], ]; for (const [path, content] of files) f.write(path, content); f.write('backend/db/migrations/meta/snapshot.json', '{}'); const sha = f.commit(); assert.deepEqual(await migrationManifest(sha, f.cwd), files.map(([path, content]) => ({ path, sha256: createHash('sha256').update(content).digest('hex'), }))); }); test('a commit without migration folders produces an empty manifest', async t => { const f = fixture(t); assert.deepEqual(await migrationManifest(f.commit(), f.cwd), []); }); test('an unreadable Git blob rejects the manifest instead of recording a partial checksum', async t => { const f = fixture(t); const path = 'backend/db/migrations/0000_missing.sql'; f.write(path, 'SELECT 1;'); const sha = f.commit(); const blob = f.run(['rev-parse', `${sha}:${path}`]); rmSync(join(f.cwd, '.git', 'objects', blob.slice(0, 2), blob.slice(2))); await assert.rejects(async () => migrationManifest(sha, f.cwd), /0000_missing\.sql/); });