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.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()}; }; 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',PORT:String(port),IHALEIST_USER_DATA_DIR:dir,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()=>{ assert.equal((await request('/api/me/tender-state','GET',null,null)).status,401); 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); 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/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'); });