guard calendar links when source time is missing

This commit is contained in:
2026-09-06 20:16:42 +03:00
parent fb89ebadae
commit 58ba35abc2
5 changed files with 46 additions and 13 deletions
@@ -33,6 +33,11 @@
"outlookStart": "2026-09-10T07:00:00Z", "outlookStart": "2026-09-10T07:00:00Z",
"outlookEnd": "2026-09-10T07:00:00Z" "outlookEnd": "2026-09-10T07:00:00Z"
}, },
"calendarMissingTimeGuard": {
"googleDisabled": true,
"outlookDisabled": true,
"providerLinks": 0
},
"analyticsTargetLabel": true, "analyticsTargetLabel": true,
"accountScopedAnalyticsTarget": true, "accountScopedAnalyticsTarget": true,
"primaryAnalyticsTargetRestored": true, "primaryAnalyticsTargetRestored": true,
+6
View File
@@ -2332,3 +2332,9 @@ Doğrulama: `npm test` 301/301, `npm run build` başarılı ve `npm run qa:brows
Takvim bağlantıları artık yalnız servis birim testlerinde değil, ihale detay modalının gerçek DOM akışında da kontrol ediliyor. Yerel QA fikstürü kaynak tarihi 10.09.2026 10:00 İstanbul olan bir ihale açıyor; Google bağlantısında `20260910T070000Z`, Outlook bağlantısında `2026-09-10T07:00:00Z` başlangıç/bitiş değerleri ve doğru provider hostları doğrulanıyor. Takvim bağlantıları artık yalnız servis birim testlerinde değil, ihale detay modalının gerçek DOM akışında da kontrol ediliyor. Yerel QA fikstürü kaynak tarihi 10.09.2026 10:00 İstanbul olan bir ihale açıyor; Google bağlantısında `20260910T070000Z`, Outlook bağlantısında `2026-09-10T07:00:00Z` başlangıç/bitiş değerleri ve doğru provider hostları doğrulanıyor.
Doğrulama: `npm run qa:browser` başarılı; kanıt [`qa-browser-acceptance-runtime.json`](evidence/qa-browser-acceptance-runtime.json) içindeki `calendarProviderLinks` alanında. Bu kabul sentetik ve salt okunur; OAuth hesabına kaydetme ve zeyilname sonrası gerçek senkronizasyon açık iş olarak kalıyor. Doğrulama: `npm run qa:browser` başarılı; kanıt [`qa-browser-acceptance-runtime.json`](evidence/qa-browser-acceptance-runtime.json) içindeki `calendarProviderLinks` alanında. Bu kabul sentetik ve salt okunur; OAuth hesabına kaydetme ve zeyilname sonrası gerçek senkronizasyon açık iş olarak kalıyor.
### 2026-09-06 · Eksik saat için takvim bağlantısı güvenliği
Kaynakta son teklif saati bulunmadığında Google/Outlook bağlantıları artık `#` gibi sahte bir hedefe gitmiyor; pasif seçenek ve açık “saat gerekli” açıklaması gösteriliyor. Geçerli tarih/saat akışı ile eksik saat akışı ayrı browser kontrollerine ayrıldı.
Doğrulama: `calendarMissingTimeGuard` içinde iki provider seçeneği pasif ve gerçek provider bağlantısı sayısı 0; `npm run qa:browser` ve `npm test` (301/301) başarılı.
+19 -10
View File
@@ -440,17 +440,26 @@ export const TenderDetailModal = () => {
<Calendar className="w-4 h-4 text-blue-600" /> Takvim Dosyasını İndir (.ics) <Calendar className="w-4 h-4 text-blue-600" /> Takvim Dosyasını İndir (.ics)
</button> </button>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<a href={calendarUrls.google || '#'} target="_blank" rel="noopener noreferrer" {calendarUrls.google ? (
onClick={(event) => handleCalendarOpen('google', event)} <a href={calendarUrls.google} target="_blank" rel="noopener noreferrer"
className="text-center bg-white hover:bg-blue-50 text-blue-700 font-bold py-2 px-3 rounded-lg text-xs border border-blue-200"> onClick={(event) => handleCalendarOpen('google', event)}
Google Takvim taslağı className="text-center bg-white hover:bg-blue-50 text-blue-700 font-bold py-2 px-3 rounded-lg text-xs border border-blue-200">
</a> Google Takvim taslağı
<a href={calendarUrls.outlook || '#'} target="_blank" rel="noopener noreferrer" </a>
onClick={(event) => handleCalendarOpen('outlook', event)} ) : (
className="text-center bg-white hover:bg-blue-50 text-blue-700 font-bold py-2 px-3 rounded-lg text-xs border border-blue-200"> <span aria-disabled="true" className="text-center bg-slate-50 text-slate-400 font-bold py-2 px-3 rounded-lg text-xs border border-slate-200">Google Takvim (saat gerekli)</span>
Outlook taslağı )}
</a> {calendarUrls.outlook ? (
<a href={calendarUrls.outlook} target="_blank" rel="noopener noreferrer"
onClick={(event) => handleCalendarOpen('outlook', event)}
className="text-center bg-white hover:bg-blue-50 text-blue-700 font-bold py-2 px-3 rounded-lg text-xs border border-blue-200">
Outlook taslağı
</a>
) : (
<span aria-disabled="true" className="text-center bg-slate-50 text-slate-400 font-bold py-2 px-3 rounded-lg text-xs border border-slate-200">Outlook (saat gerekli)</span>
)}
</div> </div>
{calendarUrls.error && <p role="status" className="text-xs text-amber-800">Takvim taslağı için kaynak tarih ve saat birlikte gerekli.</p>}
<p className="text-xs text-slate-600 mt-2">Google ve Outlook bağlantıları doğrulanmış tarih/saatle ön-doldurulmuş taslak açar; kaydetme ve değişiklik sonrası yeniden senkronizasyon kullanıcıya aittir. Otomatik OAuth senkronu yapılmaz.</p> <p className="text-xs text-slate-600 mt-2">Google ve Outlook bağlantıları doğrulanmış tarih/saatle ön-doldurulmuş taslak açar; kaydetme ve değişiklik sonrası yeniden senkronizasyon kullanıcıya aittir. Otomatik OAuth senkronu yapılmaz.</p>
</div> </div>
+12
View File
@@ -55,6 +55,18 @@ try {
outlookUrl.searchParams.get('enddt') !== '2026-09-10T07:00:00Z') { outlookUrl.searchParams.get('enddt') !== '2026-09-10T07:00:00Z') {
throw new Error(`Takvim provider bağlantısı kaynak son tarihi korumuyor: ${JSON.stringify(evidence.checks.calendarProviderLinks)}`); throw new Error(`Takvim provider bağlantısı kaynak son tarihi korumuyor: ${JSON.stringify(evidence.checks.calendarProviderLinks)}`);
} }
const missingCalendarPage = await context.newPage();
try {
await missingCalendarPage.goto(`${url.replace(/\/qa-signin$/, '')}/qa-calendar-links?missing=1`, { waitUntil: 'networkidle' });
evidence.checks.calendarMissingTimeGuard = {
googleDisabled: await missingCalendarPage.getByText('Google Takvim (saat gerekli)', { exact: true }).count() === 1,
outlookDisabled: await missingCalendarPage.getByText('Outlook (saat gerekli)', { exact: true }).count() === 1,
providerLinks: await missingCalendarPage.locator('a').filter({ hasText: /Google Takvim taslağı|Outlook taslağı/ }).count(),
};
if (!evidence.checks.calendarMissingTimeGuard.googleDisabled || !evidence.checks.calendarMissingTimeGuard.outlookDisabled || evidence.checks.calendarMissingTimeGuard.providerLinks !== 0) {
throw new Error(`Saat bilinmiyorken takvim bağlantısı pasifleşmedi: ${JSON.stringify(evidence.checks.calendarMissingTimeGuard)}`);
}
} finally { await missingCalendarPage.close(); }
} finally { await calendarPage.close(); } } finally { await calendarPage.close(); }
await page.getByRole('button', { name: 'Ana İhale Listesi', exact: true }).click(); await page.getByRole('button', { name: 'Ana İhale Listesi', exact: true }).click();
await page.getByRole('button', { name: 'Pazar', exact: true }).click(); await page.getByRole('button', { name: 'Pazar', exact: true }).click();
+4 -3
View File
@@ -140,8 +140,9 @@ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{targ
</script></body></html>`; </script></body></html>`;
res.setHeader('Content-Type','text/html; charset=utf-8');res.end(await server.transformIndexHtml('/qa-overview-frame',html));return; res.setHeader('Content-Type','text/html; charset=utf-8');res.end(await server.transformIndexHtml('/qa-overview-frame',html));return;
} }
if(req.url==='/qa-calendar-links'){ if(req.url==='/qa-calendar-links'||req.url?.startsWith('/qa-calendar-links?')){
const calendarTender={...tenders[0],ikn:'2026/1582991',tarih:'10.09.2026',saat:'10:00',baslik:'QA Giresun mikroskop',kurum:'Giresun TEST KURUMU',ihaleYeri:'QA toplantı salonu'}; const missingTime=new URL(req.url,'http://localhost').searchParams.has('missing');
const calendarTender={...tenders[0],id:missingTime?'qa-calendar-missing':'qa-calendar-valid',ikn:'2026/1582991',tarih:'10.09.2026',saat:missingTime?'':'10:00',aciklama:missingTime?'':'Kaynak tarihi doğrulama',baslik:'QA Giresun mikroskop',kurum:'Giresun TEST KURUMU',ihaleYeri:'QA toplantı salonu'};
const html=`<html lang="tr"><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><div id="root"></div><script type="module"> const html=`<html lang="tr"><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><div id="root"></div><script type="module">
import React from '/node_modules/.vite/deps/react.js'; import React from '/node_modules/.vite/deps/react.js';
import ReactDOM from '/node_modules/.vite/deps/react-dom_client.js'; import ReactDOM from '/node_modules/.vite/deps/react-dom_client.js';
@@ -152,7 +153,7 @@ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{targ
function Probe(){const {setActiveTenderDetail,authUser}=useApp();React.useEffect(()=>{if(!authUser?.email)return;const timer=setTimeout(()=>setActiveTenderDetail(selected),0);return()=>clearTimeout(timer);},[authUser?.email]);return React.createElement(TenderDetailModal);} function Probe(){const {setActiveTenderDetail,authUser}=useApp();React.useEffect(()=>{if(!authUser?.email)return;const timer=setTimeout(()=>setActiveTenderDetail(selected),0);return()=>clearTimeout(timer);},[authUser?.email]);return React.createElement(TenderDetailModal);}
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(AppProvider,{},React.createElement(Probe))); ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(AppProvider,{},React.createElement(Probe)));
</script></body></html>`; </script></body></html>`;
res.setHeader('Content-Type','text/html; charset=utf-8');res.end(await server.transformIndexHtml('/qa-calendar-links',html));return; res.setHeader('Content-Type','text/html; charset=utf-8');res.end(await server.transformIndexHtml(missingTime?'/qa-calendar-links-missing':'/qa-calendar-links',html));return;
} }
if(req.url==='/qa-source-update'||req.url==='/qa-source-update?ready=1'||req.url==='/qa-source-update?ready=1&file=1') { if(req.url==='/qa-source-update'||req.url==='/qa-source-update?ready=1'||req.url==='/qa-source-update?ready=1&file=1') {
const query=new URL(req.url,'http://localhost').searchParams; const query=new URL(req.url,'http://localhost').searchParams;