Files
ihaleist/tests/live-integrations-probe.mjs

146 lines
10 KiB
JavaScript
Raw Permalink 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.
// D09 read-only production probe. It checks public configuration, source reads
// and unauthenticated guards. It never signs in, sends a message, creates a
// checkout, calls an AI model, changes data or submits a tender.
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
const base = String(process.argv[2] || 'https://ihaleist.com').replace(/\/$/, '');
const evidencePath = process.env.IHALEIST_LIVE_INTEGRATIONS_EVIDENCE_PATH
|| 'docs/quality/evidence/qa-live-d09-integrations.json';
const timeoutMs = 45_000;
async function request(path, options = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${base}${path}`, {
...options,
signal: controller.signal,
headers: {
'User-Agent': 'ihaleist-live-integrations-probe/1.0',
...(options.headers || {}),
},
});
const contentType = response.headers.get('content-type') || '';
const body = await response.text();
let json = null;
if (contentType.includes('json')) {
try { json = JSON.parse(body); } catch { /* malformed JSON stays visible as a failed check */ }
}
return {
path,
method: options.method || 'GET',
status: response.status,
contentType,
json,
textChars: body.length,
error: response.ok ? null : String(json?.error?.message || json?.error || body || `HTTP ${response.status}`).slice(0, 300),
};
} catch (error) {
return { path, method: options.method || 'GET', status: 0, contentType: '', json: null, textChars: 0, error: error?.message || String(error) };
} finally {
clearTimeout(timer);
}
}
const raw = {};
for (const [name, path, options] of [
['google', '/api/auth/google/config'],
['tcmb', '/api/exchange-rates'],
['documents', '/api/documents/config'],
['ai', '/api/ai/config'],
['billing', '/api/billing/plans'],
['telegram', '/api/health/takip'],
['scheduler', '/api/health/scheduler'],
['items', '/api/health/kalem'],
['embedding', '/api/health/uyum'],
['bik', '/api/stats'],
['history', '/api/history/stats'],
['resultsRss', '/api/feeds/sonuc.rss?limit=1'],
['protectedRss', '/api/feeds/tenders.rss'],
['manifest', '/manifest.webmanifest'],
['serviceWorker', '/sw.js'],
['ted', '/api/ted?mod=turkiye&since=20990101&limit=1'],
['kik', '/api/kik/search?limit=1'],
['okas', '/api/okas/ara?q=hemogram'],
['mcpGuard', '/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }) }],
]) {
raw[name] = await request(path, options);
}
const guardPaths = {
authSession: ['/api/auth/me'],
documents: ['/api/me/documents'],
documentText: ['/api/documents/text?ikn=2026%2F1634449'],
apiKeys: ['/api/me/apikeys'],
emailConfig: ['/api/notify/email/config'],
plan: ['/api/me/plan'],
subscriptions: ['/api/notify/subscriptions'],
sourceHistory: ['/api/tenders/source-history?ikn=2026%2F1634449'],
companyScan: ['/api/company/scan', { method: 'POST' }],
aiAnalyze: ['/api/ai/analyze', { method: 'POST' }],
checkout: ['/api/billing/checkout', { method: 'POST' }],
emailSend: ['/api/notify/email/test', { method: 'POST' }],
telegramMutation: ['/api/me/telegram', { method: 'POST' }],
notificationRun: ['/api/notify/run', { method: 'POST' }],
};
const guards = {};
for (const [name, [path, options]] of Object.entries(guardPaths)) {
const probe = await request(path, options);
guards[name] = { status: probe.status, passed: [401, 403].includes(probe.status) };
}
function row(id, label, status, evidence, remaining) {
return { id, label, status, evidence, remaining };
}
const googleConfigured = raw.google.status === 200 && Boolean(raw.google.json?.clientId);
const tcmbReady = raw.tcmb.status === 200 && raw.tcmb.json?.success === true
&& Number(raw.tcmb.json?.rates?.USD) > 0 && Number(raw.tcmb.json?.rates?.EUR) > 0;
const aiConfigured = raw.ai.status === 200 && raw.ai.json?.configured === true;
const billingConfigured = raw.billing.status === 200 && raw.billing.json?.configured === true;
const billingWebhookReady = raw.billing.status === 200 && raw.billing.json?.webhookHazir === true;
const telegramConfigured = raw.telegram.status === 200 && raw.telegram.json?.telegram === true;
const kikManualGate = raw.kik.status === 428 && raw.kik.json?.code === 'HUMAN_VERIFICATION_REQUIRED';
const ekapManualGate = raw.okas.status === 428 && raw.okas.json?.code === 'HUMAN_VERIFICATION_REQUIRED';
const integrations = [
row('BIK', 'ilan.gov.tr ihale havuzu', raw.bik.status === 200 && raw.bik.json?.success ? 'passed' : 'blocked', { status: raw.bik.status, source: raw.bik.json?.kaynak || null, loaded: raw.bik.json?.yuklenenIlan ?? null }, 'Güncel liste okuması doğrulandı; veri kalitesi D10/D04 kapsamında izlenir.'),
row('TCMB', 'TCMB döviz satış kurları', tcmbReady ? 'passed' : 'blocked', { status: raw.tcmb.status, bulletinDate: raw.tcmb.json?.bulletinDate || null, source: raw.tcmb.json?.source || null, cached: raw.tcmb.json?.cached ?? null }, tcmbReady ? 'Hata ve eski bülten UX testleri yerel otomasyonda kapsanıyor.' : raw.tcmb.error),
row('GOOGLE', 'Google ile giriş', googleConfigured ? 'configured' : 'blocked', { status: raw.google.status, configured: googleConfigured }, 'Gerçek test hesabıyla giriş, çıkış ve oturum yenileme kabulü gerekli.'),
row('EKAP', 'EKAP arama ve resmi doküman', raw.okas.status === 200 ? 'partial' : ekapManualGate ? 'manual-gate' : 'blocked', { browserAvailable: raw.documents.json?.tarayiciVar ?? null, okasStatus: raw.okas.status, okasCode: raw.okas.json?.code || null, d10DocumentCode: 'HUMAN_VERIFICATION_REQUIRED' }, 'Yetkili kullanıcı güvenlik doğrulamasından sonra beş resmi paketi indirip yüklemeli.'),
row('KIK', 'KİK kurul kararları', raw.kik.status === 200 && raw.kik.json?.success ? 'passed' : kikManualGate ? 'manual-gate' : 'blocked', { status: raw.kik.status, code: raw.kik.json?.code || null, resultCount: raw.kik.json?.results?.length ?? null, error: raw.kik.error }, raw.kik.status === 200 ? 'Bir kararın ayrıntı bağlantısı ayrıca kullanıcı akışında açılmalı.' : kikManualGate ? 'EKAP güvenlik doğrulamasından sonra arama ve karar metni tekrar denenmeli.' : 'Kaynak erişimi düzeltildikten sonra arama ve karar metni tekrar denenmeli.'),
row('TED', 'TED Avrupa ihaleleri', raw.ted.status === 200 && raw.ted.json?.success ? 'passed' : 'blocked', { status: raw.ted.status, totalCount: raw.ted.json?.totalCount ?? null, timedOut: raw.ted.json?.timedOut ?? null, error: raw.ted.error }, raw.ted.status === 200 ? 'Gerçek sonuçlu sorgu ve çeviri, kontrollü AI test bütçesiyle yapılmalı.' : 'TED kaynağı tekrar denenmeli.'),
row('AI', 'AI analiz ve teklif yardımcıları', aiConfigured ? 'configured' : 'blocked', { status: raw.ai.status, configured: aiConfigured, providerCount: Array.isArray(raw.ai.json?.providers) ? raw.ai.json.providers.length : 0 }, 'Yetkili test hesabı ve incelemesi tamamlanmış test belgesiyle analiz, soru, taslak ve denetim zinciri gerekli.'),
row('STRIPE', 'Stripe plan, ödeme ve webhook', billingConfigured && billingWebhookReady ? 'configured' : 'blocked', { status: raw.billing.status, checkoutConfigured: billingConfigured, webhookConfigured: billingWebhookReady, planCount: Array.isArray(raw.billing.json?.plans) ? raw.billing.json.plans.length : 0 }, 'Yalnız Stripe test ortamında başarılı/başarısız ödeme, webhook tekrarı ve iptal senaryoları gerekli.'),
row('EMAIL', 'E-posta bildirimleri', guards.emailConfig?.passed ? 'guarded' : 'blocked', { unauthenticatedStatus: guards.emailConfig?.status ?? null }, 'Yetkili test hesabında yapılandırma okunmalı; izinli test alıcısına tek gönderim ayrıca onaylanmalı.'),
row('TELEGRAM', 'Telegram bağlantısı ve bildirim', telegramConfigured ? 'configured' : 'blocked', { status: raw.telegram.status, configured: telegramConfigured }, 'Test sohbetinde bağlama kodu ve tek test mesajı için açık gönderim yetkisi gerekli.'),
row('PWA', 'PWA manifest ve offline worker', raw.manifest.status === 200 && raw.serviceWorker.status === 200 ? 'passed' : 'blocked', { manifestStatus: raw.manifest.status, serviceWorkerStatus: raw.serviceWorker.status }, 'Gerçek cihazda kurulum, offline düzenleme ve yeniden bağlanma kabulü ayrıca izlenir.'),
row('API', 'API anahtarı, RSS ve MCP', raw.protectedRss.status === 401 && raw.mcpGuard.status === 401 && guards.apiKeys?.passed ? 'guarded' : 'blocked', { apiKeyGuard: guards.apiKeys?.status ?? null, rssGuard: raw.protectedRss.status, mcpGuard: raw.mcpGuard.status, publicResultsRssStatus: raw.resultsRss.status }, 'Yetkili test hesabında anahtar oluşturma, RSS/MCP kullanma ve iptal zinciri gerekli.'),
row('SCHEDULER', 'Zeyilname/sonuç zamanlayıcısı', raw.scheduler.status === 200 && raw.scheduler.json?.success ? 'observable' : 'blocked', { status: raw.scheduler.status, browserAvailable: raw.scheduler.json?.tarayiciVar ?? null, resultScanActive: raw.scheduler.json?.sonucTaramasi?.aktif ?? null, resultScanRunning: raw.scheduler.json?.sonucTaramasi?.calisiyor ?? null, directProcurementActive: raw.scheduler.json?.dogrudanTemin?.aktif ?? null, directProcurementRunning: raw.scheduler.json?.dogrudanTemin?.calisiyor ?? null }, 'Yetkili test kaydıyla zeyilname/sonuç olayı ve tekrar engeli doğrulanmalı.'),
];
const result = {
checkedAt: new Date().toISOString(),
base,
readOnly: true,
scope: 'Yapılandırma, public kaynak okuması ve kimlik doğrulama kapıları denetlendi; giriş, veri değişikliği, AI üretimi, ödeme, mesaj veya teklif gönderimi yapılmadı.',
integrations,
guards,
summary: {
total: integrations.length,
passed: integrations.filter(({ status }) => status === 'passed').length,
configured: integrations.filter(({ status }) => status === 'configured').length,
guarded: integrations.filter(({ status }) => status === 'guarded').length,
observable: integrations.filter(({ status }) => status === 'observable').length,
manualGate: integrations.filter(({ status }) => status === 'manual-gate').length,
blocked: integrations.filter(({ status }) => status === 'blocked').length,
protectedGuardsPassed: Object.values(guards).filter(({ passed }) => passed).length,
protectedGuardsTotal: Object.keys(guards).length,
},
};
await mkdir(dirname(evidencePath), { recursive: true });
await writeFile(evidencePath, `${JSON.stringify(result, null, 2)}\n`);
console.log(JSON.stringify({ checkedAt: result.checkedAt, ...result.summary, statuses: Object.fromEntries(integrations.map(({ id, status }) => [id, status])) }));