Files
ihaleist/tests/user-store.test.mjs

114 lines
6.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
try { assert.throws(() => store.setData('a@example.test','takip',[]), /test write failure/); }
finally { const cleanup=store.openDb();cleanup.exec('DROP TRIGGER reject_write');cleanup.close(); }
});
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);
});
test('data revision snapshots preserve legacy records and reject stale changes atomically',()=>{
const owner='revision@example.test',key='firma_profil';
store.setData(owner,key,{company:'Legacy'});
const first=store.getDataSnapshot(owner);
assert.deepEqual(first.veri[key],{company:'Legacy'});assert.match(first.revisions[key],/^[a-f0-9]{64}$/);
const next=store.setData(owner,key,{company:'Updated'},first.revisions[key]);assert.notEqual(next,first.revisions[key]);
assert.throws(()=>store.setData(owner,key,{company:'Stale'},first.revisions[key]),e=>e.kod==='VERSION_CONFLICT');
assert.equal(store.getAllData(owner)[key].company,'Updated');
const reopened=store.getDataSnapshot(owner);assert.equal(reopened.revisions[key],next);
assert.equal(store.setData(owner,key,{company:'Updated'},next),next);
assert.throws(()=>store.setData(owner,key,{company:'New'},null),e=>e.kod==='VERSION_CONFLICT');
});