import { test, after } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, existsSync, readFileSync, unlinkSync, mkdirSync, rmdirSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; const dir = mkdtempSync(join(tmpdir(), 'ihaleist-store-')); process.env.IHALEIST_USER_DATA_DIR = dir; const legacy = new DatabaseSync(join(dir, 'users.sqlite')); legacy.exec(`CREATE TABLE belgeler (id TEXT PRIMARY KEY,email TEXT NOT NULL,ad TEXT NOT NULL,kategori TEXT,boyut INTEGER NOT NULL,mime TEXT,dosya TEXT NOT NULL,olusturma TEXT); INSERT INTO belgeler VALUES ('old','a@example.test','Eski belge','Yasal',5,'text/plain','old.txt','2026-09-01');`); legacy.close(); const store = await import('../server/user-store.mjs'); after(() => rmSync(dir, { recursive: true, force: true })); test('old volume migrates before indexing, preserves files and migration is repeatable', () => { for (let i = 0; i < 2; i++) { const db = store.openDb(); assert.equal(db.prepare('SELECT COUNT(*) n FROM schema_migrations').get().n, 5); assert.ok(db.prepare('PRAGMA table_info(belgeler)').all().some(c => c.name === 'ikn')); db.close(); } assert.equal(store.listDocuments('a@example.test')[0].ad, 'Eski belge'); assert.equal(store.listDocuments('a@example.test')[0].tur, 'firma'); assert.deepEqual(store.listDocuments('b@example.test'), []); }); test('five preparation records persist across DB close/reopen and stay account scoped', () => { const records = Object.fromEntries(['1573235','1582991','1523851','1499061','1534665'].map(id => ['2026/'+id, { takipte: true, gorev: { kontrolListesi: [{ad:'ÜTS',tamam:false}] }, kanbanStatus:'teklif_hazirlaniyor' }])); assert.equal(store.setData('a@example.test', 'gorevler', records), true); assert.deepEqual(store.getAllData('a@example.test').gorevler, records); assert.equal(store.getData('b@example.test', 'gorevler'), null); }); test('document and API key ownership enforced', () => { const id = store.addDocument('a@example.test', {ad:'Şartname',boyut:10,dosya:'spec.pdf',ikn:'2026/1582991'}); assert.equal(store.listDocuments('a@example.test', {ikn:'2026/1582991'}).length, 1); assert.equal(store.getDocument('b@example.test', id), null); const key = store.createApiKey('a@example.test', 'Test'); assert.equal(store.verifyApiKey(key.anahtar).email, 'a@example.test'); assert.equal(store.revokeApiKey('b@example.test', key.id), false); store.revokeApiKey('a@example.test', key.id); assert.equal(store.verifyApiKey(key.anahtar), null); }); test('database write failure propagates instead of pretending success', () => { const db = store.openDb(); db.exec(`CREATE TRIGGER reject_write BEFORE INSERT ON kullanici_verisi BEGIN SELECT RAISE(FAIL,'test write failure'); END;`); db.close(); assert.throws(() => store.setData('a@example.test','takip',[]), /test write failure/); }); test('per-tender versions prevent stale updates; five tasks persist', () => { for (const id of ['1573235','1582991','1523851','1499061','1534665']) { const key = '2026/'+id; const saved = store.saveTenderState('c@example.test', key, {takipte:true,gorev:{kontrolListesi:[]}}, {id:key,ikn:key}, 0); assert.equal(saved.version,1); assert.throws(() => store.saveTenderState('c@example.test', key, {takipte:false}, {}, 0), e => e.kod === 'VERSION_CONFLICT'); assert.equal(store.saveTenderState('c@example.test', key, {notes:[{text:'test'}]}, {}, 1).version,2); } assert.equal(Object.keys(store.getTenderStates('c@example.test')).length,5); assert.deepEqual(store.getTenderStates('d@example.test'),{}); }); test('API key quota is transactional and revoked slots can be reused', () => { const email='quota@example.test'; const key=store.createApiKey(email,' ERP ',{limit:1}); assert.equal(key.ad,'ERP'); assert.throws(()=>store.createApiKey(email,'excess',{limit:1}), {kod:'KOTA_ASILDI'}); assert.equal(store.listApiKeys(email).length,1); assert.equal(store.revokeApiKey(email,key.id),true); assert.ok(store.createApiKey(email,'replacement',{limit:1}).id); assert.equal(store.listApiKeys(email).length,1); for(const ad of [{},[],42,'x'.repeat(121)])assert.throws(()=>store.createApiKey('invalid-name@example.test',ad)); assert.deepEqual(store.listApiKeys('invalid-name@example.test'),[]); }); test('failed document deletion leaves the original bytes and metadata intact',()=>{ const email='delete-rollback@example.test', bytes=Buffer.from('Korunması gereken belge'); const id=store.saveDocumentFile(email,{ad:'rollback.txt'},bytes); const file=join(store.BELGE_DIR,store.getDocument(email,id).dosya); const db=store.openDb(); db.exec("CREATE TRIGGER reject_doc_delete BEFORE DELETE ON belgeler BEGIN SELECT RAISE(FAIL,'delete rejected'); END;");db.close(); try { assert.throws(()=>store.deleteDocument(email,id),/delete rejected/); assert.deepEqual(readFileSync(file),bytes); assert.ok(store.getDocument(email,id)); } finally {const db=store.openDb();db.exec('DROP TRIGGER reject_doc_delete');db.close();} assert.equal(store.deleteDocument(email,id),true); assert.equal(existsSync(file),false); }); test('committed deletion remains inaccessible and pending physical cleanup survives reopen',()=>{ const email='cleanup@example.test'; const id=store.saveDocumentFile(email,{ad:'cleanup.txt'},Buffer.from('QA')); const file=join(store.BELGE_DIR,store.getDocument(email,id).dosya); unlinkSync(file);mkdirSync(file); // force unlink failure without relying on OS permissions assert.equal(store.deleteDocument(email,id),true); assert.equal(store.getDocument(email,id),null); const db=store.openDb();assert.equal(db.prepare('SELECT COUNT(*) n FROM document_file_cleanup').get().n,1);db.close(); rmdirSync(file);writeFileSync(file,'QA cleanup recovered'); const reopened=store.openDb();assert.equal(reopened.prepare('SELECT COUNT(*) n FROM document_file_cleanup').get().n,0);reopened.close(); assert.equal(existsSync(file),false); });