Files
ihaleist/tests/http-state.test.mjs
T

332 lines
26 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,before,after} from 'node:test';
import assert from 'node:assert/strict';
import {spawn} from 'node:child_process';
import {mkdtempSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {randomBytes} from 'node:crypto';
import {createServer} from 'node:net';
const dir=mkdtempSync(join(tmpdir(),'ihaleist-http-'));
process.env.IHALEIST_USER_DATA_DIR=dir;
process.env.AUTH_SECRET=randomBytes(32).toString('hex');
const {signToken}=await import('../server/auth.mjs');
const token=signToken({email:'qa@example.test',name:'QA'});
const other=signToken({email:'other@example.test',name:'Other'});
let child,base;
const request=async(path,method='GET',body,auth=token)=>{
const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json',...(auth?{Authorization:'Bearer '+auth}:{})},body:body?JSON.stringify(body):undefined});
return {status:r.status,data:await r.json(),headers:r.headers};
};
before(async()=>{
const socket=createServer();await new Promise(r=>socket.listen(0,'127.0.0.1',r));const port=socket.address().port;await new Promise(r=>socket.close(r));
base='http://127.0.0.1:'+port;
child=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{...process.env,NODE_ENV:'test',CORS_ORIGINS:'https://client.example,https://admin.example',TELEGRAM_BOT_TOKEN:'',PORT:String(port),IHALEIST_USER_DATA_DIR:dir,IHALEIST_SHARED_DATA_DIR:join(dir,'shared'),UYUM_GOMME:'0'},stdio:'ignore'});
for(let i=0;i<100;i++){try{await fetch(base+'/api/auth/google/config');return;}catch{await new Promise(r=>setTimeout(r,50));}}
throw new Error('QA API did not start');
});
after(async()=>{if(child){child.kill();await new Promise(r=>child.once('exit',r));}rmSync(dir,{recursive:true,force:true});});
test('HTTP preparation requires auth, persists draft and rejects stale revisions',async()=>{
const preflight=await fetch(base+'/api/me/plan',{method:'OPTIONS',headers:{Origin:'https://client.example', 'Access-Control-Request-Method':'GET', 'Access-Control-Request-Headers':'authorization,content-type'}});
assert.equal(preflight.status,204);
assert.equal(preflight.headers.get('access-control-allow-origin'),'https://client.example');
assert.equal(preflight.headers.get('vary'),'Origin');
assert.equal(preflight.headers.get('access-control-allow-headers'),'Authorization,Content-Type');
const denied=await fetch(base+'/api/me/plan',{method:'OPTIONS',headers:{Origin:'https://untrusted.example','Access-Control-Request-Method':'GET'}});
assert.equal(denied.status,204);assert.equal(denied.headers.get('access-control-allow-origin'),null);
const unauthorized=await request('/api/me/tender-state','GET',null,null);
assert.equal(unauthorized.status,401);
assert.equal(unauthorized.headers.get('x-content-type-options'),'nosniff');
assert.equal(unauthorized.headers.get('referrer-policy'),'no-referrer');
const draft={firma:{firmaUnvan:'QA Laboratuvar'},priceSchema:1,pricesBySource:{kit1:'12.50'},gecerlilikGun:'120',checks:{products:{kit1:{product:'Test kit',status:'suitable',source:'Teknik 3.2'}},documents:{doc1:{status:'verified',evidence:'Test v1'}}},manualDocuments:[{document:'Yetki',note:'7.1',mandatory:true}],analysisSource:{itemsAndSpecs:[{name:'Kit',quantity:'2 adet'}],documentChecklist:[]}};
const data={key:'2026/1582991',patch:{takipte:true,gorev:{kontrolListesi:[]},bidDraft:draft,checklist:[{key:'collect-documents',sourceSnapshot:'source-v1',done:true}]},snapshot:{id:'ilan-qa',ikn:'2026/1582991'},version:0};
assert.equal((await request('/api/me/tender-state','PUT',data)).status,200);
const read=await request('/api/me/tender-state');assert.deepEqual(read.data.records[data.key].data.bidDraft,draft);
assert.deepEqual(read.data.records[data.key].data.checklist,data.patch.checklist);
assert.equal((await request('/api/me/tender-state','PUT',data)).status,409);
assert.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{});
});
test('HTTP documents and API keys expose empty successful state, not endless load errors',async()=>{
const docs=await request('/api/me/documents');assert.equal(docs.status,200);assert.deepEqual(docs.data.belgeler,[]);
const keys=await request('/api/me/apikeys');assert.equal(keys.status,200);assert.deepEqual(keys.data.anahtarlar,[]);
const data=await request('/api/me/data');assert.equal(data.status,200);assert.deepEqual(data.data.veri,{});
});
test('HTTP file upload, owned download, metadata update and deletion operate on actual bytes',async()=>{
const bytes=Buffer.from('QA belge — Türkçe içerik, yalnız izole test.');
for (const invalid of ['!!!!','a','a===','AB==',[],{}]) {
assert.equal((await request('/api/me/documents','POST',{ad:'broken.txt',icerikBase64:invalid})).status,400);
}
const uploaded=await request('/api/me/documents','POST',{ad:'qa.txt',mime:'text/plain',icerikBase64:bytes.toString('base64'),kategori:'Firma',sonKullanma:'2026-12-31'});
assert.equal(uploaded.status,200);
const id=uploaded.data.id;
const list=await request('/api/me/documents');const file=list.data.belgeler.find(f=>f.id===id);
assert.equal(file.erisilebilir,true);assert.equal(file.boyut,bytes.length);assert.equal(file.dosya,undefined);
const downloaded=await fetch(base+'/api/me/documents/download?id='+id,{headers:{Authorization:'Bearer '+token}});
assert.equal(downloaded.status,200);assert.deepEqual(Buffer.from(await downloaded.arrayBuffer()),bytes);
assert.equal((await request('/api/me/documents/download?id='+id,'GET',null,other)).status,404);
assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:'2027-01-01'})).status,200);
const changed=await request('/api/me/documents');assert.equal(changed.data.belgeler.find(f=>f.id===id).son_kullanma,'2027-01-01');
for (const invalid of ['2027-02-29','2026-02-31','2026-12-31suffix',42]) {
assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:invalid})).status,400);
assert.equal((await request('/api/me/documents','POST',{ad:'invalid.txt',icerikBase64:bytes.toString('base64'),sonKullanma:invalid})).status,400);
}
const categoryOnly=await request('/api/me/documents/meta','PATCH',{id,kategori:'Yeterlik'});
assert.equal(categoryOnly.data.belgeler.find(f=>f.id===id).son_kullanma,'2027-01-01');
assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:null},other)).status,404);
const cleared=await request('/api/me/documents/meta','PATCH',{id,sonKullanma:null});
assert.equal(cleared.data.belgeler.find(f=>f.id===id).son_kullanma,null);
assert.equal(cleared.data.belgeler.find(f=>f.id===id).kategori,'Yeterlik');
assert.equal((await request('/api/me/documents')).data.belgeler.length,1);
assert.equal((await request('/api/me/documents?id='+id,'DELETE')).status,200);
assert.equal((await request('/api/me/documents/download?id='+id)).status,404);
});
test('shared item extraction requires an owned tender document and matching tender',async()=>{
const body={ad:'scope.txt',icerikBase64:Buffer.from('Yalnız test dokümanı, cetvel yok.').toString('base64'),ikn:'2026/123456'};
const company=await request('/api/me/documents','POST',body);
assert.equal(company.status,200);
assert.equal(company.data.processing.status,'not_applicable');
assert.equal(company.data.belgeler.find(d=>d.id===company.data.id).tur,'firma');
const extract=id=>({documentId:id,ikn:body.ikn,metin:'Cetvel yok.'});
assert.equal((await request('/api/kalem/cikar','POST',extract(company.data.id))).status,403);
await request('/api/me/documents/meta','PATCH',{id:company.data.id,kategori:'İhale Dokümanı'});
assert.equal((await request('/api/kalem/cikar','POST',extract(company.data.id))).status,403);
const tender=await request('/api/me/documents','POST',{...body,documentType:'ihale'});
assert.equal(tender.status,200);
assert.equal(tender.data.processing.status,'awaiting_text');
const processing=async()=> (await request('/api/me/documents')).data.belgeler.find(d=>d.id===tender.data.id).isleme;
assert.equal((await request('/api/kalem/cikar','POST',extract(tender.data.id),other)).status,403);
assert.equal((await request('/api/kalem/cikar','POST',{...extract(tender.data.id),ikn:'2026/999999'})).status,403);
assert.equal((await processing()).status,'awaiting_text');
const prior=await processing();
const readError={id:tender.data.id,expectedProcessing:prior};
assert.equal((await request('/api/me/documents/read-error','POST',readError,null)).status,401);
assert.equal((await request('/api/me/documents/read-error','POST',readError,other)).status,404);
assert.equal((await request('/api/me/documents/read-error','POST',{id:company.data.id,expectedProcessing:null})).status,404);
assert.equal((await request('/api/me/documents/read-error','POST',readError)).status,200);
assert.equal((await processing()).status,'error');
assert.equal((await request('/api/me/documents/read-error','POST',readError)).status,409);
assert.equal((await request('/api/kalem/cikar','POST',{...extract(tender.data.id),metin:{invalid:true}})).status,400);
assert.equal((await processing()).status,'error');
assert.equal((await request('/api/kalem/cikar','POST',extract(tender.data.id))).status,200);
assert.equal((await processing()).status,'no_items');
assert.equal((await processing()).count,0);
assert.ok((await processing()).updatedAt);
assert.equal((await request('/api/me/documents')).data.belgeler.filter(d=>d.id===tender.data.id).length,1);
const retained=await fetch(base+'/api/me/documents/download?id='+tender.data.id,{headers:{Authorization:'Bearer '+token}});
assert.equal(retained.status,200);
assert.equal(await retained.text(),Buffer.from(body.icerikBase64,'base64').toString());
const reviewUrl='/api/me/documents/text-review?id='+tender.data.id;
assert.equal((await request(reviewUrl,'GET',null,other)).status,404);
const initialReview=await request(reviewUrl);
assert.equal(initialReview.data.review,null);
const correction={id:tender.data.id,fileHash:initialReview.data.fileHash,expectedRevision:0,
correctedText:'Sentetik belge metni tüm sayfalarla karşılaştırıldı. Bu gerçek ihale incelemesi değildir.',
note:'Sayfa 1 üzerinde sentetik test karşılaştırması yapıldı.',allPagesCompared:true};
assert.equal((await request('/api/me/documents/text-review','POST',{...correction,allPagesCompared:false})).status,400);
assert.equal((await request('/api/me/documents/text-review','POST',correction,other)).status,404);
const savedReview=await request('/api/me/documents/text-review','POST',correction);
assert.equal(savedReview.status,200);assert.equal(savedReview.data.review.revision,1);
const verification={ikn:body.ikn,review:{documentId:tender.data.id,revision:1,fileHash:savedReview.data.fileHash,textHash:savedReview.data.review.text_hash}};
assert.equal((await request('/api/me/documents/text-review/validate','POST',verification)).status,200);
assert.equal((await request('/api/me/documents/text-review/validate','POST',verification,other)).status,404);
assert.equal((await request('/api/me/documents/text-review/validate','POST',{...verification,review:{...verification.review,textHash:'wrong'}})).status,409);
assert.equal((await request('/api/me/documents/text-review/validate','POST',{...verification,ikn:'2026/999'})).status,409);
assert.equal((await request(reviewUrl)).data.review.corrected_text,correction.correctedText);
assert.equal((await request('/api/me/documents/text-review','POST',correction)).status,409);
assert.equal((await request('/api/me/documents/text-review','POST',{...correction,expectedRevision:1,fileHash:'old-hash'})).status,409);
assert.equal((await request('/api/me/documents/text-review','POST',{...correction,expectedRevision:1,correctedText:correction.correctedText+' İkinci inceleme.'})).status,200);
assert.equal((await request('/api/me/documents/text-review/validate','POST',verification)).status,409);
assert.equal((await request('/api/kalem/cikar','POST',{ikn:body.ikn,metin:'test'})).status,403);
await request('/api/me/documents?id='+company.data.id,'DELETE');
await request('/api/me/documents?id='+tender.data.id,'DELETE');
});
test('HTTP API key creation, private listing, MCP use and revocation form a complete lifecycle', async () => {
const admin=signToken({email:'keys@example.test',name:'Key QA',isAdmin:true});
const created=await request('/api/me/apikeys','POST',{ad:'QA integration'},admin);
assert.equal(created.status,200);
const {id,anahtar}=created.data.anahtar;
const listed=await request('/api/me/apikeys','GET',null,admin);
assert.equal(listed.data.anahtarlar.length,1);
assert.equal(listed.data.anahtarlar[0].anahtar,undefined);
assert.equal(listed.data.anahtarlar[0].hash,undefined);
assert.equal((await request('/api/me/apikeys?id='+id,'DELETE',null,other)).status,404);
const rpc={jsonrpc:'2.0',id:1,method:'initialize',params:{}};
assert.equal((await request('/mcp','POST',rpc,anahtar)).status,200);
const used=await request('/api/me/apikeys','GET',null,admin);
assert.ok(used.data.anahtarlar[0].son_kullanim);
const revoked=await request('/api/me/apikeys?id='+id,'DELETE',null,admin);
assert.equal(revoked.status,200);
assert.equal(revoked.data.limit,listed.data.limit);
assert.deepEqual(revoked.data.anahtarlar,[]);
assert.equal((await request('/mcp','POST',rpc,anahtar)).status,401);
});
test('concurrent API key requests never exceed quota and malformed input creates nothing',async()=>{
const auth=signToken({email:'quota-http@example.test',name:'Quota QA',isAdmin:true});
const pending=Array.from({length:16},(_,i)=>request('/api/me/apikeys','POST',{ad:`QA ${i}`},auth));
const results=await Promise.all(pending);
assert.equal(results.filter(r=>r.status===200).length,10);
assert.equal(results.filter(r=>r.status===402&&r.data.kod==='KOTA_ASILDI').length,6);
const keys=await request('/api/me/apikeys','GET',null,auth);
assert.equal(keys.data.anahtarlar.length,10);
await request('/api/me/apikeys?id='+keys.data.anahtarlar[0].id,'DELETE',null,auth);
const invalid=await fetch(base+'/api/me/apikeys',{method:'POST',headers:{Authorization:'Bearer '+auth,'Content-Type':'application/json'},body:'{broken'});
assert.equal(invalid.status,400);
assert.equal((await request('/api/me/apikeys','POST',{ad:{}},auth)).status,400);
assert.equal((await request('/api/me/apikeys','GET',null,auth)).data.anahtarlar.length,9);
assert.equal((await request('/api/me/apikeys','POST',{ad:'replacement'},auth)).status,200);
assert.equal((await request('/api/me/apikeys','PUT',{},auth)).status,405);
});
test('source history requires authentication and distinguishes empty results from invalid pagination',async()=>{
assert.equal((await request('/api/tenders/source-history?ikn=2026/123','GET',null,null)).status,401);
const response=await request('/api/tenders/source-history?ikn=2026/123');
assert.equal(response.status,200);assert.deepEqual(response.data.records,[]);assert.equal(response.data.nextCursor,null);
assert.equal((await request('/api/tenders/source-history?ikn=2026/123&before=bad')).status,400);
assert.equal((await request('/api/tenders/source-history?ikn=2026/123','POST',{})).status,405);
});
test('durable AI job endpoints enforce account isolation without starting provider calls',async()=>{
assert.equal((await request('/api/me/ai-jobs','GET',null,null)).status,401);
const list=await request('/api/me/ai-jobs?ikn=2026/123');assert.equal(list.status,200);assert.deepEqual(list.data.jobs,[]);
assert.equal((await request('/api/me/ai-jobs?id=missing')).status,404);
assert.equal((await request('/api/me/ai-jobs?id=missing','DELETE',null,other)).status,404);
assert.equal((await request('/api/me/ai-jobs','PUT',{})).status,405);
});
test('outbox review endpoint requires administrator and rejects unsupported or invalid changes',async()=>{
assert.equal((await request('/api/admin/notify/outbox','GET',null,null)).status,401);
assert.equal((await request('/api/admin/notify/outbox')).status,403);
assert.equal((await request('/api/admin/notify/outbox','POST',{id:'x',action:'skip',note:'Bu kullanıcı yönetici değildir.'})).status,403);
const admin=signToken({email:'outbox-admin@example.test',name:'Admin',isAdmin:true});
assert.equal((await request('/api/admin/notify/outbox','GET',null,admin)).status,200);
assert.equal((await request('/api/admin/notify/outbox','DELETE',null,admin)).status,405);
assert.equal((await request('/api/admin/notify/outbox','POST',{id:'x',action:'skip',note:'short'},admin)).status,400);
assert.equal((await request('/api/admin/notify/outbox','POST',{id:'missing',action:'skip',note:'Sağlayıcı kaydı kontrol edildi ve kapatılıyor.'},admin)).status,409);
});
test('profile and saved-filter writes require the loaded revision and reject concurrent overwrites',async()=>{
for(const anahtar of ['firma_profil','saved_filters']){
const deger=anahtar==='firma_profil'?{company:'QA başlangıç'}:[{id:'qa',name:'Başlangıç'}];
assert.equal((await request('/api/me/data','PUT',{anahtar,deger})).status,428);
const initial=await request('/api/me/data');const revision=initial.data.revisions[anahtar] ?? null;
const first=await request('/api/me/data','PUT',{anahtar,deger,revision});assert.equal(first.status,200);assert.match(first.data.revision,/^[a-f0-9]{64}$/);
const attempts=await Promise.all(Array.from({length:8},(_,i)=>request('/api/me/data','PUT',{anahtar,deger:anahtar==='firma_profil'?{company:'QA '+i}:[{id:'qa'+i}],revision:first.data.revision})));
assert.equal(attempts.filter(x=>x.status===200).length,1);
assert.equal(attempts.filter(x=>x.status===409&&x.data.kod==='VERSION_CONFLICT').length,7);
const after=await request('/api/me/data');assert.equal(after.data.revisions[anahtar],attempts.find(x=>x.status===200).data.revision);
const stale=await request('/api/me/data','PUT',{anahtar,deger,revision:first.data.revision});assert.equal(stale.status,409);
assert.deepEqual((await request('/api/me/data')).data.veri[anahtar],after.data.veri[anahtar]);
// A revision read under another owner cannot grant write access to this record.
const foreign=await request('/api/me/data','PUT',{anahtar,deger,revision:after.data.revisions[anahtar]},other);assert.equal(foreign.status,409);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger,revision:null},other)).status,200);
}
});
test('health portfolio writes reject malformed records before persistence',async()=>{
const current=await request('/api/me/data');
const revision=current.data.revisions.firma_profil ?? null;
const base={company:'QA sağlık',scrapedCapabilities:{healthPortfolio:[{id:'hp-1',group:'Mikroskop',kind:'device',utsStatus:'declared'}]}};
assert.equal((await request('/api/me/data','PUT',{anahtar:'firma_profil',deger:base,revision})).status,200);
const next=(await request('/api/me/data')).data.revisions.firma_profil;
for (const healthPortfolio of [[{id:'hp-1'},{id:'hp-1'}],[{id:'hp-1'},{id:' hp-1 '}],[{id:'hp-1',kind:'other'}],[{id:'hp-1',model:'x'.repeat(1001)}]]) {
const response=await request('/api/me/data','PUT',{anahtar:'firma_profil',deger:{...base,scrapedCapabilities:{healthPortfolio}},revision:next});
assert.equal(response.status,400);
}
assert.deepEqual((await request('/api/me/data')).data.veri.firma_profil,base);
});
test('team membership rejects stale and over-quota writes and permits removing last member',async()=>{
const anahtar='ekip',member={id:'qa-team',name:'QA ekip üyesi',role:'Uzman',email:''};
const initial=await request('/api/me/data');
const revision=initial.data.revisions.ekip ?? null;
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:{},revision})).status,400);
const saved=await request('/api/me/data','PUT',{anahtar,deger:[member],revision});assert.equal(saved.status,200);
const stale=await request('/api/me/data','PUT',{anahtar,deger:[],revision});assert.equal(stale.status,409);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[member,{...member,id:'qa-2'}],revision:saved.data.revision})).status,402);
assert.deepEqual((await request('/api/me/data')).data.veri.ekip,[member]);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:saved.data.revision})).status,200);
assert.deepEqual((await request('/api/me/data')).data.veri.ekip,[]);
});
test('favorite institutions require a version and retain current values after a stale update',async()=>{
const anahtar='favori_kurumlar';
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:['QA Kurum']})).status,428);
const first=await request('/api/me/data','PUT',{anahtar,deger:['QA Kurum'],revision:null});assert.equal(first.status,200);
const updated=await request('/api/me/data','PUT',{anahtar,deger:['QA Kurum','QA Diğer'],revision:first.data.revision});assert.equal(updated.status,200);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:first.data.revision})).status,409);
for(const bad of [{},[null],[' '],['QA Kurum',' QA Kurum ']])assert.equal((await request('/api/me/data','PUT',{anahtar,deger:bad,revision:updated.data.revision})).status,400);
assert.deepEqual((await request('/api/me/data')).data.veri[anahtar],['QA Kurum','QA Diğer']);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:updated.data.revision})).status,200);
});
test('competitor watchlist keeps the saved roster when a stale or invalid write is rejected',async()=>{
const anahtar='rakip_izleme',item={id:'qa-radar',name:'QA Medikal',sector:'Sağlık'};
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[item]})).status,428);
const first=await request('/api/me/data','PUT',{anahtar,deger:[item],revision:null});assert.equal(first.status,200);
for(const deger of [{},[{}],[{...item,sector:{}}],[item,{...item,id:'other',name:' qa medikal '}]])
assert.equal((await request('/api/me/data','PUT',{anahtar,deger,revision:first.data.revision})).status,400);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:null})).status,409);
assert.deepEqual((await request('/api/me/data')).data.veri[anahtar],[item]);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:first.data.revision})).status,200);
});
test('notification preferences reject stale writes and malformed quiet times',async()=>{
const anahtar='reminder',deger={pushEnabled:true,quietEnabled:true,quietStart:'22:00',quietEnd:'08:00'};
const first=await request('/api/me/data','PUT',{anahtar,deger,revision:null});assert.equal(first.status,200);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:{...deger,quietStart:'25:00'},revision:first.data.revision})).status,400);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:{...deger,pushEnabled:false},revision:null})).status,409);
assert.deepEqual((await request('/api/me/data')).data.veri.reminder,deger);
});
test('weekly brief preference loads without feed preview and changes only with a current revision',async()=>{
const first=await request('/api/me/brief');assert.equal(first.status,200);assert.equal(first.data.kapali,false);assert.equal(first.data.revision,null);assert.equal(first.data.metin,undefined);
assert.equal((await request('/api/me/brief','POST',{})).status,400);
const saved=await request('/api/me/brief','POST',{kapali:true,revision:null});assert.equal(saved.status,200);assert.equal(saved.data.kapali,true);
assert.equal((await request('/api/me/brief','POST',{kapali:false,revision:null})).status,409);
assert.equal((await request('/api/me/brief')).data.kapali,true);
assert.equal((await request('/api/me/brief','GET',null,other)).data.kapali,false);
assert.equal((await request('/api/me/brief','POST',{kapali:false,revision:saved.data.revision})).status,200);
});
test('weekly brief review is admin-only and persists evidence without sending messages',async()=>{
const route='/api/admin/brief/outbox';
assert.equal((await request(route,'GET',null,null)).status,401);
assert.equal((await request(route)).status,403);
assert.equal((await request(route,'POST',{id:'x',action:'skip',note:'Yetkisiz işlem kontrolü.'})).status,403);
const admin=signToken({email:'weekly-admin@example.test',name:'Admin',isAdmin:true});
const {createBriefOutbox}=await import('../server/brief-outbox.mjs');
const box=createBriefOutbox(dir),batch=box.prepare('2026-09-07','weekly-test@example.test',[{channel:'email',to:'weekly-test@example.test'}]);
box.hold(box.claim(batch.id,0),'Test sonucu belirsiz');
const list=await request(route,'GET',null,admin);
assert.equal(list.status,200);assert.equal(list.data.deliveries.length,1);
const id=list.data.deliveries[0].id;
assert.equal((await request(route,'DELETE',null,admin)).status,405);
const input={id,action:'confirmed_sent',providerId:'test-provider-receipt',confirmed:true,note:'İzole test sağlayıcı kaydı doğrulandı.'};
assert.equal((await request(route,'POST',{...input,confirmed:false},admin)).status,400);
assert.equal((await request(route,'POST',input,admin)).status,200);
assert.equal((await request(route,'POST',input,admin)).status,409);
const saved=(await request(route,'GET',null,admin)).data.deliveries[0];
assert.equal(saved.review.actor,'weekly-admin@example.test');assert.equal(saved.providerId,input.providerId);
assert.equal(saved.state,'sent_manual');
});
test('Telegram cannot be linked through generic data writes or legacy client codes',async()=>{
assert.equal((await request('/api/me/data','PUT',{anahtar:'telegram',deger:{chatId:'123'}})).status,403);
assert.equal((await request('/api/me/telegram','POST',{kod:'123456'})).status,400);
assert.equal((await request('/api/me/telegram','PATCH',{})).status,405);
assert.equal((await request('/api/me/telegram','POST',{islem:'kod'},null)).status,401);
});
test('saved Telegram link does not imply an unconfigured test message was sent',async()=>{
const {createTelegramLinker}=await import('../server/telegram-link.mjs');
// Parent and HTTP child use the same disposable database; no real provider credentials.
const tgStore=await import('../server/user-store.mjs');
const linker=createTelegramLinker(tgStore),email='telegram-http@example.test';
const challenge=linker.issue(email);await linker.link(email,challenge.kod,async()=>({chatId:'123',ad:'Synthetic'}));
const session=signToken({email,name:'Telegram Test'});
const result=await request('/api/me/telegram','POST',{islem:'test'},session);
assert.equal(result.status,503);assert.equal(result.data.success,false);
assert.equal(result.data.id,undefined);
});