100 lines
5.4 KiB
JavaScript
100 lines
5.4 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import { createBriefOutbox, briefWeekKey } from '../server/brief-outbox.mjs';
|
|
import { createBriefSender } from '../server/brief-delivery.mjs';
|
|
import { backupDataVolume, restoreDataVolume } from '../server/backup-data-volume.mjs';
|
|
|
|
function fixture(t) {
|
|
const root=mkdtempSync(join(tmpdir(),'brief-outbox-')),directory=join(root,'data');
|
|
t.after(()=>rmSync(root,{recursive:true,force:true}));
|
|
return {root,directory,outbox:createBriefOutbox(directory)};
|
|
}
|
|
function sender(outbox, overrides={}) {
|
|
return createBriefSender({outbox,clock:()=>new Date('2026-09-07T05:00:00Z'),
|
|
store:{listUsers:()=>[{email:'test@example.test'}],getData:()=>null},
|
|
buildBrief:()=>({bos:false,hafta:'07 Eylül 2026'}),text:()=> 'Test',html:()=> '<p>Test</p>',
|
|
telegramConfigured:()=>false,sendTelegram:async()=>true,emailConfigured:()=>true,
|
|
sendEmail:async()=>({id:'synthetic-receipt'}),warn:()=>{},...overrides});
|
|
}
|
|
|
|
test('accepted delivery survives reopening and does not send again; weeks and owners are isolated',async t=>{
|
|
const {outbox,directory}=fixture(t);let calls=0;
|
|
const transport={sendEmail:async()=>{calls++;return {id:'receipt'};}};
|
|
assert.equal((await sender(outbox,transport)(async()=>[])).gonderilen,1);
|
|
const again=await sender(createBriefOutbox(directory),transport)(async()=>[]);
|
|
assert.equal(calls,1);assert.equal(again.atlanan,1);
|
|
const next=sender(outbox,{...transport,clock:()=>new Date('2026-09-14T05:00:00Z')});
|
|
await next(async()=>[]);assert.equal(calls,2);
|
|
const other=outbox.prepare('2026-09-07','other@example.test',[{text:'Test'}]);
|
|
assert.equal(outbox.claim(other.id,0).state,'sending');
|
|
});
|
|
|
|
test('concurrent senders cannot call the provider for the same message twice',async t=>{
|
|
const {outbox,directory}=fixture(t);let release;
|
|
const gate=new Promise(resolve=>{release=resolve;});let entered;
|
|
const ready=new Promise(resolve=>{entered=resolve;});let calls=0;
|
|
const transport={sendEmail:async()=>{calls++;entered();await gate;return {id:'receipt'};}};
|
|
const first=sender(outbox,transport)(async()=>[]);
|
|
await ready;
|
|
const second=await sender(createBriefOutbox(directory),transport)(async()=>[]);
|
|
assert.equal(second.bekleyen,1);assert.equal(calls,1);
|
|
release();await first;
|
|
assert.equal(outbox.inspect()[0].state,'accepted');
|
|
});
|
|
|
|
test('ambiguous provider failure and changed payload remain held without automatic replay',async t=>{
|
|
const {outbox}=fixture(t);let calls=0;
|
|
const transport={sendEmail:async()=>{calls++;throw new Error('Connection lost after send');}};
|
|
assert.equal((await sender(outbox,transport)(async()=>[])).hata,1);
|
|
const retry=await sender(outbox,transport)(async()=>[]);
|
|
assert.equal(retry.bekleyen,1);assert.equal(calls,1);
|
|
const changed=await sender(outbox,{text:()=> 'Changed'})(async()=>[]);
|
|
assert.equal(changed.hata,1);assert.equal(changed.bekleyen,1);
|
|
assert.equal(outbox.inspect()[0].state,'uncertain');
|
|
});
|
|
|
|
test('feed failure creates no completion record and a subsequent fetch can proceed',async t=>{
|
|
const {outbox}=fixture(t),send=sender(outbox);
|
|
await assert.rejects(send(async()=>{throw new Error('Feed down');}),/Feed down/);
|
|
assert.deepEqual(outbox.inspect(),[]);
|
|
assert.equal((await send(async()=>[])).gonderilen,1);
|
|
});
|
|
|
|
test('SIGKILL after claiming leaves an uncertain message after lease expiry, never a fresh send',async t=>{
|
|
const {directory,outbox}=fixture(t);
|
|
const moduleUrl=new URL('../server/brief-outbox.mjs',import.meta.url).href;
|
|
const code=`import {createBriefOutbox} from ${JSON.stringify(moduleUrl)};
|
|
const box=createBriefOutbox(process.argv[1]);
|
|
const batch=box.prepare('2026-09-07','test@example.test',[{text:'Test'}]);
|
|
box.claim(batch.id,0,1000);process.stdout.write(batch.id+'\\n');setInterval(()=>{},1000);`;
|
|
const child=spawn(process.execPath,['--input-type=module','-e',code,directory],{stdio:['ignore','pipe','pipe']});
|
|
t.after(()=>{if(child.exitCode===null)child.kill('SIGKILL');});
|
|
const output=await Promise.race([once(child.stdout,'data'),once(child,'exit').then(()=>{throw new Error('Child exited before claim');})]);
|
|
const batch=output[0].toString().trim();
|
|
const stopped=once(child,'exit');child.kill('SIGKILL');await stopped;
|
|
assert.equal(outbox.claim(batch,0,2000).state,'busy');
|
|
assert.equal(outbox.claim(batch,0,121001).state,'uncertain');
|
|
assert.equal(outbox.claim(batch,0,999999).state,'uncertain');
|
|
});
|
|
|
|
test('full-volume backup restores accepted message records and prevents replay',async t=>{
|
|
const {root,directory,outbox}=fixture(t);
|
|
await sender(outbox)(async()=>[]);
|
|
const backup=join(root,'backup'),restored=join(root,'restored');
|
|
const manifest=await backupDataVolume(directory,backup,{offline:true});
|
|
assert.ok(manifest.files.some(file=>file.name==='brief.sqlite'&&file.sqlite));
|
|
await restoreDataVolume(backup,restored);
|
|
const send=sender(createBriefOutbox(restored),{sendEmail:async()=>{throw new Error('Must not replay');}});
|
|
const result=await send(async()=>[]);assert.equal(result.atlanan,1);assert.equal(result.hata,0);
|
|
});
|
|
|
|
test('weekly id uses Monday in Istanbul even while UTC is still Sunday',()=>{
|
|
assert.equal(briefWeekKey(new Date('2026-09-06T20:59:59Z')),'2026-08-31');
|
|
assert.equal(briefWeekKey(new Date('2026-09-06T21:00:00Z')),'2026-09-07');
|
|
});
|