verify browser account data isolation

This commit is contained in:
2026-09-06 19:32:39 +03:00
parent 24898f8384
commit e3e9c2d35a
4 changed files with 52 additions and 0 deletions
@@ -27,9 +27,13 @@
], ],
"dailyBulletinHeading": true, "dailyBulletinHeading": true,
"analyticsTargetLabel": true, "analyticsTargetLabel": true,
"accountScopedAnalyticsTarget": true,
"primaryAnalyticsTargetRestored": true,
"icsImported": true, "icsImported": true,
"icsStorageScoped": true, "icsStorageScoped": true,
"icsPersistsAfterTabChange": true, "icsPersistsAfterTabChange": true,
"accountScopedIcs": true,
"primaryIcsRestored": true,
"readyPreparationStatus": 200, "readyPreparationStatus": 200,
"readyOutputButtons": 4, "readyOutputButtons": 4,
"readyOutputButtonsEnabled": true, "readyOutputButtonsEnabled": true,
+6
View File
@@ -2272,3 +2272,9 @@ Doğrulama: yeni regresyonla otomatik test sayısı 295/295’e yükseldi; üret
İlan metni ayrıştırıcısı artık EKAP ve kurum ilanlarında kullanılan “İhale tarihi ve saati”, “Son Teklif Tarihi” ve “İhale Tarihi/Saati” başlıklarını aynı tarih-saat alanına dönüştürüyor. Ayrıştırma yalnız tarih ve saat birlikte bulunduğunda çalışıyor; teslim süresi gibi yalnız tarih içeren maddeler son teklif tarihi olarak yanlış eşleştirilmiyor. İlan metni ayrıştırıcısı artık EKAP ve kurum ilanlarında kullanılan “İhale tarihi ve saati”, “Son Teklif Tarihi” ve “İhale Tarihi/Saati” başlıklarını aynı tarih-saat alanına dönüştürüyor. Ayrıştırma yalnız tarih ve saat birlikte bulunduğunda çalışıyor; teslim süresi gibi yalnız tarih içeren maddeler son teklif tarihi olarak yanlış eşleştirilmiyor.
Doğrulama: üç yaygın başlık varyantı için regresyonlar eklendi; 296 otomatik test ve üretim derlemesi başarılı. Resmî beş ihale dosyasıyla uçtan uca doğrulama ve canlı deployment hâlâ açık. Doğrulama: üç yaygın başlık varyantı için regresyonlar eklendi; 296 otomatik test ve üretim derlemesi başarılı. Resmî beş ihale dosyasıyla uçtan uca doğrulama ve canlı deployment hâlâ açık.
### 2026-09-06 · D01 iki hesaplı tarayıcı kabulü
Yerel browser QA fikstürüne ikinci geçerli test hesabı eklendi. Birinci hesaba ait analitik ciro hedefi ve ICS etkinliği ikinci hesapta görünmüyor; hesap birinciye döndüğünde her iki kayıt da geri yükleniyor. Böylece istemci tarafındaki hesap kapsamlı anahtarların yalnız statik kod ve birim testte değil, gerçek oturum değişimi ve yeniden yükleme akışında da çalıştığı doğrulanıyor.
Doğrulama: `npm run qa:browser` başarılı; `accountScopedAnalyticsTarget`, `primaryAnalyticsTargetRestored`, `accountScopedIcs` ve `primaryIcsRestored` kanıtları [`qa-browser-acceptance-runtime.json`](evidence/qa-browser-acceptance-runtime.json) içinde. Kapsam sentetik yerel QA’dır; gerçek iki firma hesabı, production verisi ve offline kabulü hâlâ açık.
+32
View File
@@ -39,6 +39,24 @@ try {
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 }); await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.analyticsTargetLabel = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).count() === 1; evidence.checks.analyticsTargetLabel = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).count() === 1;
if (!evidence.checks.analyticsTargetLabel) throw new Error('Analitik ciro hedefi alanı erişilebilir ad taşımıyor.'); if (!evidence.checks.analyticsTargetLabel) throw new Error('Analitik ciro hedefi alanı erişilebilir ad taşımıyor.');
const primaryTarget = page.getByLabel('Ciro hedefi (TL)', { exact: true });
await primaryTarget.fill('1250');
await page.waitForFunction(() => localStorage.getItem('ihaleist_ciro_hedefi:browser-qa%40example.test') === '1250');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=other`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Pazar', exact: true }).click();
await page.getByRole('button', { name: 'Analitik Dashboard', exact: true }).click();
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.accountScopedAnalyticsTarget = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).inputValue() === '';
if (!evidence.checks.accountScopedAnalyticsTarget) throw new Error('İkinci hesap ilk hesabın ciro hedefini devraldı.');
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).fill('980');
await page.waitForFunction(() => localStorage.getItem('ihaleist_ciro_hedefi:browser-qa-other%40example.test') === '980');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=primary`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Pazar', exact: true }).click();
await page.getByRole('button', { name: 'Analitik Dashboard', exact: true }).click();
await page.getByRole('button', { name: 'Ciro hedefini düzenle', exact: true }).click();
await page.getByLabel('Ciro hedefi (TL)', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.primaryAnalyticsTargetRestored = await page.getByLabel('Ciro hedefi (TL)', { exact: true }).inputValue() === '1250';
if (!evidence.checks.primaryAnalyticsTargetRestored) throw new Error('İlk hesabın ciro hedefi hesap değişiminden sonra geri yüklenmedi.');
await page.getByRole('button', { name: 'Keşfet', exact: true }).click(); await page.getByRole('button', { name: 'Keşfet', exact: true }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click(); await page.getByRole('button', { name: 'İhale Takvimi' }).click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
@@ -55,6 +73,20 @@ try {
await page.getByRole('button', { name: 'İhale Takvimi' }).click(); await page.getByRole('button', { name: 'İhale Takvimi' }).click();
evidence.checks.icsPersistsAfterTabChange = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 1; evidence.checks.icsPersistsAfterTabChange = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 1;
if (!evidence.checks.icsPersistsAfterTabChange) throw new Error('ICS kaydı sekme değişiminden sonra korunmadı.'); if (!evidence.checks.icsPersistsAfterTabChange) throw new Error('ICS kaydı sekme değişiminden sonra korunmadı.');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=other`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Keşfet' }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click();
evidence.checks.accountScopedIcs = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 0;
if (!evidence.checks.accountScopedIcs) throw new Error('İkinci hesap ilk hesabın ICS etkinliğini devraldı.');
await page.goto(`${url.replace(/\/qa-signin$/, '')}/qa-switch-account?account=primary`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Keşfet' }).click();
await page.getByRole('button', { name: 'İhale Takvimi' }).click();
await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).waitFor({ state: 'attached', timeout: 10_000 });
evidence.checks.primaryIcsRestored = await page.getByText('1 harici etkinlik içe aktarıldı.', { exact: true }).count() === 1;
if (!evidence.checks.primaryIcsRestored) {
const storage = await page.evaluate(() => Object.fromEntries(Object.keys(sessionStorage).map(key => [key, sessionStorage.getItem(key)])));
throw new Error(`İlk hesabın ICS etkinliği hesap değişiminden sonra geri yüklenmedi: ${JSON.stringify(storage)}`);
}
} finally { await rm(icsDir, { recursive: true, force: true }); } } finally { await rm(icsDir, { recursive: true, force: true }); }
const readyPage = await context.newPage(); const readyPage = await context.newPage();
await readyPage.setViewportSize({ width: 1440, height: 1000 }); await readyPage.setViewportSize({ width: 1440, height: 1000 });
+10
View File
@@ -15,6 +15,8 @@ process.env.AUTH_SECRET=randomBytes(32).toString('hex');
const {signToken}=await import('../server/auth.mjs'); const {signToken}=await import('../server/auth.mjs');
const user={email:'browser-qa@example.test',name:'Laboratuvar QA',isAdmin:false}; const user={email:'browser-qa@example.test',name:'Laboratuvar QA',isAdmin:false};
const token=signToken(user); const token=signToken(user);
const otherUser={email:'browser-qa-other@example.test',name:'İkinci Laboratuvar QA',isAdmin:false};
const otherToken=signToken(otherUser);
const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true}; const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true};
const keyToken=signToken(keyUser); const keyToken=signToken(keyUser);
let apiKeyListFailure=false; let apiKeyListFailure=false;
@@ -170,6 +172,14 @@ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{targ
if(req.url?.startsWith('/qa-brief-failure?')&&req.method==='POST'){briefFailure=new URL(req.url,'http://localhost').searchParams.get('mode');res.end('Özet test durumu: '+briefFailure);return;} if(req.url?.startsWith('/qa-brief-failure?')&&req.method==='POST'){briefFailure=new URL(req.url,'http://localhost').searchParams.get('mode');res.end('Özet test durumu: '+briefFailure);return;}
if(req.url?.startsWith('/api/me/brief')&&((briefFailure==='read'&&req.method==='GET')||(briefFailure==='write'&&req.method==='POST'))){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Haftalık özet deposuna erişilemiyor.'}));return;} if(req.url?.startsWith('/api/me/brief')&&((briefFailure==='read'&&req.method==='GET')||(briefFailure==='write'&&req.method==='POST'))){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Haftalık özet deposuna erişilemiyor.'}));return;}
if(req.url==='/qa-api-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel API testi</h1><p>Yalnız geçici test deposunu etkiler.</p><button id="login">API test hesabıyla gir</button><button id="fail">Liste hatasını aç</button><button id="recover">Liste hatasını kapat</button><p role="status"></p><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(keyToken)},${JSON.stringify(keyUser)});location.href='/';};for(const [id,mode] of [['fail','on'],['recover','off']])document.getElementById(id).onclick=async()=>{await fetch('/qa-api-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=mode==='on'?'Liste hatası açık':'Liste hatası kapalı';};</script></html>`);return;} if(req.url==='/qa-api-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel API testi</h1><p>Yalnız geçici test deposunu etkiler.</p><button id="login">API test hesabıyla gir</button><button id="fail">Liste hatasını aç</button><button id="recover">Liste hatasını kapat</button><p role="status"></p><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(keyToken)},${JSON.stringify(keyUser)});location.href='/';};for(const [id,mode] of [['fail','on'],['recover','off']])document.getElementById(id).onclick=async()=>{await fetch('/qa-api-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=mode==='on'?'Liste hatası açık':'Liste hatası kapalı';};</script></html>`);return;}
if(req.url?.startsWith('/qa-switch-account?')) {
const account=new URL(req.url,'http://localhost').searchParams.get('account');
const selected=account==='other'?otherUser:user;
const selectedToken=account==='other'?otherToken:token;
res.setHeader('Content-Type','text/html; charset=utf-8');
res.end(`<html lang="tr"><body><p>QA hesap değişimi</p><script type="module">import {setSession} from '/src/services/auth.js';setSession(${JSON.stringify(selectedToken)},${JSON.stringify(selected)});location.replace('/');</script></body></html>`);
return;
}
if(req.method==='POST' && req.url?.startsWith('/qa-api-failure?')) {apiKeyListFailure=new URL(req.url,'http://localhost').searchParams.get('mode')==='on';res.end('ok');return;} if(req.method==='POST' && req.url?.startsWith('/qa-api-failure?')) {apiKeyListFailure=new URL(req.url,'http://localhost').searchParams.get('mode')==='on';res.end('ok');return;}
if(apiKeyListFailure && req.method==='GET' && req.url==='/api/me/apikeys') {res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Anahtar deposuna erişilemiyor.'}));return;} if(apiKeyListFailure && req.method==='GET' && req.url==='/api/me/apikeys') {res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Anahtar deposuna erişilemiyor.'}));return;}
if(req.url==='/qa-signin') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel laboratuvar testi</h1><p>Sentetik ihale verileri ve geçici kullanıcı deposu kullanılır.</p><button id="login">Test hesabıyla gir</button><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(token)},${JSON.stringify(user)});location.href='/';};</script></html>`);return;} if(req.url==='/qa-signin') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel laboratuvar testi</h1><p>Sentetik ihale verileri ve geçici kullanıcı deposu kullanılır.</p><button id="login">Test hesabıyla gir</button><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(token)},${JSON.stringify(user)});location.href='/';};</script></html>`);return;}