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

137 lines
11 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.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,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()=>{
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);
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');
});