import test,{after} from 'node:test'; import assert from 'node:assert/strict'; import {mkdtempSync,rmSync,statSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; const dir=mkdtempSync(join(tmpdir(),'ihaleist-subscriptions-'));process.env.IHALEIST_USER_DATA_DIR=dir; const subscriptions=await import('../server/notify-store.mjs'); after(()=>rmSync(dir,{recursive:true,force:true})); test('subscription writes validate criteria and deletion is account scoped in the configured volume',()=>{ for(const criteria of [[],{konular:'lab'},{konular:[{}]},{kurumlar:['']},{sehir:[]}])assert.throws(()=>subscriptions.subscribe('owner',criteria),/Geçersiz/); assert.deepEqual(subscriptions.list('owner'),[]); const rows=subscriptions.subscribe('owner',{konular:['Laboratuvar'],sehir:'Ankara'}); assert.equal(rows.length,1);assert.deepEqual(rows[0].konular,['Laboratuvar']); assert.deepEqual(subscriptions.list('other'),[]);assert.equal(subscriptions.remove(rows[0].id,'other'),false); assert.equal(subscriptions.list('owner').length,1); assert.equal(statSync(join(dir,'notify.sqlite')).mode&0o777,0o600); assert.equal(subscriptions.remove(rows[0].id,'owner'),true);assert.deepEqual(subscriptions.list('owner'),[]); }); test('distinct topic and institution sets coexist while equivalent subscriptions retain delivery history',()=>{ const one=subscriptions.subscribe('sets',{konular:['Laboratuvar','TEST'],kurumlar:['Hastane A']})[0]; subscriptions.markSent(one.id,['tender-1']); subscriptions.subscribe('sets',{konular:['test',' laboratuvar '],kurumlar:['hastane a','Hastane A']}); let rows=subscriptions.list('sets');assert.equal(rows.length,1);assert.equal(rows[0].id,one.id);assert.deepEqual(rows[0].sonGorulenler,['tender-1']); subscriptions.subscribe('sets',{konular:['Cihaz'],kurumlar:['Hastane A']}); subscriptions.subscribe('sets',{konular:['Laboratuvar','TEST'],kurumlar:['Hastane B']}); rows=subscriptions.list('sets');assert.equal(rows.length,3);assert.equal(rows.find(row=>row.id===one.id).sonGorulenler[0],'tender-1'); }); test('baseline does not claim delivery and seen history is not truncated at 400',()=>{ const row=subscriptions.subscribe('history',{konular:['Laboratuvar']})[0]; subscriptions.markBaseline(row.id,Array.from({length:450},(_,i)=>'old-'+i)); let saved=subscriptions.list('history')[0];assert.equal(saved.initialized,true);assert.equal(saved.sonGonderim,null);assert.equal(saved.sonGorulenler.length,450); subscriptions.markSent(row.id,['new-1','old-1']);saved=subscriptions.list('history')[0]; assert.equal(saved.sonGorulenler.length,451);assert.ok(saved.sonGonderim);assert.ok(saved.sonGorulenler.includes('old-0')); }); test('outbox pins payload and key, fences workers, and atomically acknowledges only its tender ids',()=>{ const sub=subscriptions.subscribe('outbox',{konular:['Laboratuvar']})[0];subscriptions.markBaseline(sub.id,[]); const first=subscriptions.reserveDelivery(sub.id,{to:'outbox',html:'source A',from:'sender A'},['a'],{clock:1000}); assert.equal(subscriptions.reserveDelivery(sub.id,{html:'source B'},['b'],{clock:1001}),null); const reclaimed=subscriptions.reserveDelivery(sub.id,{html:'source B'},['b'],{clock:122000}); assert.equal(reclaimed.id,first.id);assert.notEqual(reclaimed.lease,first.lease);assert.equal(reclaimed.payload.html,'source A'); assert.throws(()=>subscriptions.acceptDelivery(first,'late-provider'),/artık etkin/); assert.equal(subscriptions.failDelivery(first,new Error('late failure')),false); subscriptions.failDelivery(reclaimed,new Error('provider response lost')); const retry=subscriptions.reserveDelivery(sub.id,null,[],{clock:123000}); assert.equal(retry.id,first.id);subscriptions.acceptDelivery(retry,'provider-id'); const saved=subscriptions.list('outbox')[0];assert.deepEqual(saved.sonGorulenler,['a']);assert.equal(saved.deliveryPending,false); assert.equal(subscriptions.reserveDelivery(sub.id,{html:'stale snapshot'},['a'],{clock:124000}),null); }); test('uncertain outbox past safe retry window blocks replay, and unsubscribe cancels pending delivery',()=>{ const sub=subscriptions.subscribe('uncertain',{konular:['Test']})[0]; const first=subscriptions.reserveDelivery(sub.id,{html:'A'},['a'],{clock:1000}); subscriptions.failDelivery(first,'unknown'); assert.deepEqual(subscriptions.reserveDelivery(sub.id,{html:'B'},['b'],{clock:1000+23*3600000}),{blocked:true}); assert.equal(subscriptions.list('uncertain')[0].deliveryStatus.state,'uncertain'); assert.equal(subscriptions.remove(sub.id,'uncertain'),true); assert.equal(subscriptions.reserveDelivery(sub.id,{html:'B'},['b']),null); assert.throws(()=>subscriptions.acceptDelivery(first,'late'),/artık etkin/); }); test('notifier resumes exact saved email after provider failure even when current source changes',async()=>{ const {bildirimleriIsle}=await import('../server/notifier.mjs'); const sub=subscriptions.subscribe('integration',{konular:['Lab']})[0];subscriptions.markBaseline(sub.id,[]); const tender={id:'integrated',tarih:'10.09.2026',saat:'10:00',baslik:'Lab source A'};let fail=true;const calls=[]; const dependencies={now:()=>new Date('2026-09-06T00:00:00Z'),configured:()=>true,telegramReady:()=>false,readSubscriptions:()=>subscriptions.list('integration'),deliverEmail:async mail=>{calls.push(mail);if(fail)throw new Error('unknown provider result');return {id:'integrated-provider'};}}; await bildirimleriIsle(async()=>({tenders:[tender]}),{},dependencies);fail=false; await bildirimleriIsle(async()=>({tenders:[{...tender,baslik:'Lab source B'}]}),{},dependencies); assert.equal(calls.length,2);assert.deepEqual(calls[0],calls[1]);assert.ok(calls[0].idempotencyKey.startsWith('notify/')); assert.deepEqual(subscriptions.list('integration')[0].sonGorulenler,['integrated']); }); for(const action of ['confirmed_sent','skip'])test(`manual outbox ${action} is audited, closes only uncertain work and prevents replay`,()=>{ const email='review-'+action; const sub=subscriptions.subscribe(email,{konular:['Lab']})[0];subscriptions.markBaseline(sub.id,[]); const delivery=subscriptions.reserveDelivery(sub.id,{to:email,subject:'Review',html:'A'},['review-tender'],{clock:1}); assert.throws(()=>subscriptions.resolveDelivery(delivery.id,'admin',{action,note:'Kontrol edilen sağlayıcı kaydı açıklaması.',providerId:'provider-proof',confirmed:true}),/inceleme beklemiyor/); subscriptions.reserveDelivery(sub.id,null,[],{clock:23*3600000+1}); assert.throws(()=>subscriptions.resolveDelivery(delivery.id,'admin',{action,note:'short'}),/20 karakter/); if(action==='confirmed_sent')assert.throws(()=>subscriptions.resolveDelivery(delivery.id,'admin',{action,note:'Kontrol edilen sağlayıcı kaydı açıklaması.'}),/kontrol onayı/); subscriptions.resolveDelivery(delivery.id,'admin',{action,note:'Kontrol edilen sağlayıcı kaydı açıklaması.',providerId:'provider-proof',confirmed:true}); const row=subscriptions.list(email)[0];assert.equal(row.deliveryPending,false);assert.deepEqual(row.sonGorulenler,['review-tender']);assert.equal(!!row.sonGonderim,action==='confirmed_sent'); const review=subscriptions.deliveryQueue().find(row=>row.id===delivery.id);assert.equal(review.review.actor,'admin');assert.equal(review.review.action,action);assert.equal(review.state,action==='skip'?'skipped':'sent_manual'); assert.throws(()=>subscriptions.resolveDelivery(delivery.id,'admin',{action,note:'İkinci karar yazılmamalıdır açıklaması.',providerId:'provider-proof',confirmed:true}),/inceleme beklemiyor/); assert.equal(subscriptions.reserveDelivery(sub.id,{to:email,html:'A'},['review-tender']),null); assert.equal(subscriptions.remove(sub.id,email),true);assert.equal(subscriptions.deliveryQueue().find(row=>row.id===delivery.id).review.action,action); });