Files
ihaleist/tests/subscription-store.test.mjs
T

102 lines
8.7 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,{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);
});
test('source review hold preserves payload and seen state, fences stale workers and retains reason',()=>{
const sub=subscriptions.subscribe('source-review',{konular:['Laboratuvar']})[0];
subscriptions.markBaseline(sub.id,[]);
const first=subscriptions.reserveDelivery(sub.id,{html:'immutable'},['pending-item'],{clock:1000});
const reclaimed=subscriptions.reserveDelivery(sub.id,{html:'changed'},['new-item'],{clock:122000});
assert.equal(subscriptions.holdDelivery(first,'stale worker'),false);
assert.equal(subscriptions.holdDelivery(reclaimed,'Kaynak tarihi değişti; inceleme gerekli.'),true);
assert.equal(subscriptions.failDelivery(reclaimed,'late failure'),false);
assert.throws(()=>subscriptions.acceptDelivery(reclaimed,'late-provider'),/etkin değil/);
assert.deepEqual(subscriptions.reserveDelivery(sub.id,{html:'new'},['new-item'],{clock:123000}),{blocked:true});
assert.deepEqual(subscriptions.list('source-review')[0].sonGorulenler,[]);
assert.equal(subscriptions.deliveryQueue().find(row=>row.id===first.id).error,'Kaynak tarihi değişti; inceleme gerekli.');
});