validate health portfolio profile writes

This commit is contained in:
2026-09-06 18:07:10 +03:00
parent 64919270d8
commit 231c372411
3 changed files with 36 additions and 0 deletions
+6
View File
@@ -1964,3 +1964,9 @@ Doğrulama: 284 test geçti, üretim derlemesi başarılı.
Telegram kanalı üretimde artık yalnızca BotFather biçimindeki tokenlarla etkin görünüyor; eksik veya placeholder değerler entegrasyonu yanlışlıkla aktif göstermiyor. Test/geliştirme ortamlarında sentetik tokenlarla mevcut entegrasyon testleri korunuyor. Telegram kanalı üretimde artık yalnızca BotFather biçimindeki tokenlarla etkin görünüyor; eksik veya placeholder değerler entegrasyonu yanlışlıkla aktif göstermiyor. Test/geliştirme ortamlarında sentetik tokenlarla mevcut entegrasyon testleri korunuyor.
Doğrulama: 285 test geçti, üretim derlemesi başarılı. Doğrulama: 285 test geçti, üretim derlemesi başarılı.
### 2026-09-06 · Sağlık portföyü sunucu doğrulaması
Firma profilindeki sağlık ürün/hizmet portföyü artık sunucuya yazılmadan önce kayıt kimliği, tekrar kayıt, alan uzunluğu, kapsam ve ÜTS beyanı açısından doğrulanıyor. Hatalı portföy verisi kalıcı kaydı veya uygunluk akışını bozamıyor.
Doğrulama: 286 test geçti, üretim derlemesi başarılı.
+18
View File
@@ -73,6 +73,23 @@ const CA_BUNDLE = [...tls.rootCertificates, EXTRA_CA];
const PORT = process.env.PORT || 3001; const PORT = process.env.PORT || 3001;
const ILAN_BASE = 'https://www.ilan.gov.tr'; const ILAN_BASE = 'https://www.ilan.gov.tr';
function validateHealthPortfolio(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Firma profili geçersiz.');
const portfolio = value.scrapedCapabilities?.healthPortfolio;
if (portfolio === undefined) return;
if (!Array.isArray(portfolio) || portfolio.length > 200) throw new Error('Sağlık ürün portföyü geçersiz veya çok uzun.');
const ids = new Set();
for (const item of portfolio) {
if (!item || typeof item !== 'object' || Array.isArray(item) || typeof item.id !== 'string' || !item.id.trim() || ids.has(item.id))
throw new Error('Sağlık ürün portföyünde geçersiz veya yinelenen kayıt var.');
ids.add(item.id);
for (const key of ['group','brand','model','utsReference','serviceCapacity'])
if (item[key] !== undefined && (typeof item[key] !== 'string' || item[key].length > 1000)) throw new Error('Sağlık ürün portföyü alanı geçersiz.');
if (!['consumable','device','staffedService'].includes(item.kind || 'consumable')) throw new Error('Sağlık ürün portföyü kapsamı geçersiz.');
if (!['unknown','declared','notApplicable'].includes(item.utsStatus || 'unknown')) throw new Error('ÜTS beyanı geçersiz.');
}
}
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 dk const CACHE_TTL_MS = 10 * 60 * 1000; // 10 dk
const LIST_SIZE = 20; // ilan.gov.tr sayfa başına 20 kayıtla sınırlıyor const LIST_SIZE = 20; // ilan.gov.tr sayfa başına 20 kayıtla sınırlıyor
// ilan.gov.tr sayfa başına 20 kayıt veriyor; havuzun tamamı için çok sayfa gerekir. // ilan.gov.tr sayfa başına 20 kayıt veriyor; havuzun tamamı için çok sayfa gerekir.
@@ -1241,6 +1258,7 @@ const server = http.createServer(async (req, res) => {
return json(res,400,{success:false,error:'Favori kurum listesi geçersiz veya yinelenen kurum içeriyor.'}); return json(res,400,{success:false,error:'Favori kurum listesi geçersiz veya yinelenen kurum içeriyor.'});
if(k==='rakip_izleme' && (!Array.isArray(body.deger) || body.deger.some(item=>!item || typeof item.id!=='string'||!item.id||typeof item.name!=='string'||!item.name.trim()||item.name.length>1000||(item.sector!=null&&typeof item.sector!=='string')) || new Set(body.deger.map(item=>item.id)).size!==body.deger.length || new Set(body.deger.map(item=>item.name.trim().toLocaleLowerCase('tr-TR'))).size!==body.deger.length)) if(k==='rakip_izleme' && (!Array.isArray(body.deger) || body.deger.some(item=>!item || typeof item.id!=='string'||!item.id||typeof item.name!=='string'||!item.name.trim()||item.name.length>1000||(item.sector!=null&&typeof item.sector!=='string')) || new Set(body.deger.map(item=>item.id)).size!==body.deger.length || new Set(body.deger.map(item=>item.name.trim().toLocaleLowerCase('tr-TR'))).size!==body.deger.length))
return json(res,400,{success:false,error:'Rakip listesi geçersiz veya yinelenen firma içeriyor.'}); return json(res,400,{success:false,error:'Rakip listesi geçersiz veya yinelenen firma içeriyor.'});
if(k==='firma_profil') validateHealthPortfolio(body.deger);
if(k==='reminder')validateReminderPreferences(body.deger); if(k==='reminder')validateReminderPreferences(body.deger);
if(k==='brief'&&typeof body.deger?.kapali!=='boolean')return json(res,400,{success:false,error:'Haftalık özet seçimi geçersiz.'}); if(k==='brief'&&typeof body.deger?.kapali!=='boolean')return json(res,400,{success:false,error:'Haftalık özet seçimi geçersiz.'});
const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder','brief'].includes(k); const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder','brief'].includes(k);
+12
View File
@@ -224,6 +224,18 @@ test('profile and saved-filter writes require the loaded revision and reject con
assert.equal((await request('/api/me/data','PUT',{anahtar,deger,revision:null},other)).status,200); 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',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()=>{ 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 anahtar='ekip',member={id:'qa-team',name:'QA ekip üyesi',role:'Uzman',email:''};
const initial=await request('/api/me/data'); const initial=await request('/api/me/data');