Files
ihaleist/tests/brief-outbox.test.mjs
T

149 lines
9.0 KiB
JavaScript
Raw 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 } 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');
});
test('review needs provider evidence, persists actor and closes only the selected message',t=>{
const {outbox,directory}=fixture(t);
const batch=outbox.prepare('2026-09-07','review@example.test',[{channel:'email',to:'review@example.test'},{channel:'telegram',chatId:'test'}]);
const first=outbox.claim(batch.id,0,1000),second=outbox.claim(batch.id,1,1000);
outbox.hold(first,'Unknown response');outbox.hold(second,'Unknown response');
const id=`${batch.id}:0`,input={action:'confirmed_sent',note:'Sağlayıcı kayıtları incelendi ve eşleşti.',providerId:'provider-test-id',confirmed:true};
assert.throws(()=>outbox.resolve(id,'admin@example.test',{...input,confirmed:false}),/onayı/);
assert.throws(()=>outbox.resolve(id,'admin@example.test',{...input,providerId:''}),/kimliği/);
assert.throws(()=>outbox.resolve(id,'admin@example.test',{...input,note:'short'}),/açıklaması/);
assert.equal(outbox.resolve(id,'admin@example.test',input).state,'sent_manual');
assert.throws(()=>outbox.resolve(id,'other-admin@example.test',input),{status:409});
const rows=createBriefOutbox(directory).queue(10000),done=rows.find(row=>row.id===id);
assert.equal(done.review.actor,'admin@example.test');assert.equal(done.review.note,input.note);
assert.equal(done.providerId,'provider-test-id');assert.equal(done.state,'sent_manual');
assert.equal(rows.find(row=>row.id!==id).state,'uncertain');
assert.equal(outbox.claim(batch.id,0).state,'accepted');
});
test('skip never claims delivery and a fully reviewed changed plan cannot start new messages',t=>{
const {outbox}=fixture(t);
const batch=outbox.prepare('2026-09-07','review@example.test',[{channel:'email'}]);
outbox.hold(outbox.claim(batch.id,0),'Uncertain');
outbox.resolve(`${batch.id}:0`,'admin@example.test',{action:'skip',note:'Sonuç doğrulanamadı, tekrar gönderilmeden kapatıldı.'});
assert.equal(outbox.claim(batch.id,0).state,'skipped');
const row=outbox.queue()[0];assert.equal(row.providerId,null);assert.equal(row.state,'skipped');
assert.equal(outbox.prepare('2026-09-07','review@example.test',[{channel:'email',text:'Changed'},{channel:'telegram'}]).closed,true);
});
test('review queue exposes expired interrupted sends but does not allow reviewing a live claim',t=>{
const {outbox}=fixture(t);
const batch=outbox.prepare('2026-09-07','review@example.test',[{channel:'email'}]);
outbox.claim(batch.id,0,1000);
assert.equal(outbox.queue(2000)[0].state,'sending');
assert.throws(()=>outbox.resolve(`${batch.id}:0`,'admin@example.test',{action:'skip',note:'Bu gönderim halen etkin durumdadır.'}),{status:409});
assert.equal(outbox.queue(121001)[0].state,'uncertain');
});
test('Telegram provider receipt is persisted for each message and missing receipt stays uncertain',async t=>{
const {outbox}=fixture(t);
const telegram={store:{listUsers:()=>[{email:'telegram@example.test'}],getData:(email,key)=>key==='telegram'?{chatId:'123'}:null},
telegramConfigured:()=>true,emailConfigured:()=>false,sendTelegram:async()=>({id:'123:42',chatId:'123',messageId:42})};
const sent=await sender(outbox,telegram)(async()=>[]);
assert.equal(sent.gonderilen,1);assert.equal(outbox.inspect()[0].receipt,'123:42');
const missing=await sender(outbox,{...telegram,clock:()=>new Date('2026-09-14T05:00:00Z'),sendTelegram:async()=>true})(async()=>[]);
assert.equal(missing.hata,1);assert.equal(missing.gonderilen,0);
const uncertain=outbox.inspect().find(row=>row.week==='2026-09-14');
assert.equal(uncertain.state,'uncertain');assert.equal(uncertain.receipt,null);
});