import { test, after } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { tenderSnapshot } from '../src/services/tenderState.js'; const dir = mkdtempSync(join(tmpdir(), 'ihaleist-feed-')); process.env.IHALEIST_USER_DATA_DIR = dir; const store = await import('../server/user-store.mjs'); const { ihaleFiltrele, yaklasanIhaleler, trTarih } = await import('../server/feeds-mcp.mjs'); const { briefMetni, briefHtml, briefScheduleKey } = await import('../server/brief.mjs'); after(() => rmSync(dir, { recursive: true, force: true })); const email = 'dates@example.test'; const rows = [ { id:'past', ikn:'2026/1', tarih:'06.09.2026', saat:'00:00' }, { id:'future', ikn:'2026/2', tarih:'06.09.2026', saat:'10:00' }, { id:'unknown', ikn:'2026/3', tarih:'06.09.2026', saat:'' }, { id:'invalid', ikn:'2026/4', tarih:'31.09.2026', saat:'10:00' }, { id:'unfollowed', ikn:'2026/5', tarih:'07.09.2026', saat:'10:00' }, ]; const now = new Date('2026-09-05T21:30:00Z'); // Istanbul Sep 6, 00:30; UTC still Sep 5. test('RSS and MCP use Istanbul deadlines regardless of server timezone', () => { const previous = process.env.TZ; try { for (const zone of ['UTC', 'America/Los_Angeles', 'Europe/Istanbul']) { process.env.TZ = zone; assert.deepEqual(ihaleFiltrele(rows, {}, now).map(t => t.id), ['future','unknown','invalid','unfollowed']); assert.equal(trTarih('6/9/2026').toISOString(), '2026-09-05T21:00:00.000Z'); assert.equal(trTarih('31.09.2026'), null); assert.equal(trTarih('06.09.2026 garbage'), null); } } finally { if (previous === undefined) delete process.env.TZ; else process.env.TZ = previous; } }); test('upcoming API reads saved follows, rejects stale unfollows and includes out-of-pool snapshots', () => { store.setData(email, 'takip', ['2026/5']); for (const t of rows) store.saveTenderState(email, t.ikn, { takipte: t.id !== 'unfollowed' }, t, 0); const outside = { id:'outside', ikn:'2026/6', tarih:'07.09.2026', saat:'09:00' }; store.saveTenderState(email, outside.ikn, { takipte:true }, outside, 0); const result = yaklasanIhaleler(email, rows, 14, now); assert.deepEqual(result.map(t => t.ikn), ['2026/2','2026/3','2026/6']); assert.equal(result[0].kalan_gun, 0); assert.equal(result[0].son_teklif_zamani, '2026-09-06T07:00:00.000Z'); assert.equal(result[1].saat_dogrulanmali, true); assert.equal(result[1].son_teklif_zamani, null); assert.equal(result[2].kalan_gun, 1); assert.deepEqual(yaklasanIhaleler('other@example.test', rows, 14, now), []); assert.ok(!store.getTrackedTenderKeys(email).includes('2026/5')); const brief = { hafta:'Test haftası', yaklasan:result, sonuclar:[] }; for (const output of [briefMetni(brief), briefHtml(brief)]) { assert.match(output, /10:00 \(Türkiye saati\)/); assert.match(output, /saat doğrulanmalı/); } }); test('weekly notification schedule starts at Monday 08:00 Istanbul', () => { assert.equal(briefScheduleKey(new Date('2026-09-07T04:59:59Z')), null); assert.equal(briefScheduleKey(new Date('2026-09-07T05:00:00Z')), '2026-8-7'); assert.equal(briefScheduleKey(new Date('2026-09-07T20:59:59Z')), '2026-8-7'); assert.equal(briefScheduleKey(new Date('2026-09-07T21:00:00Z')), null); }); test('closed tenders stay out of open feeds and weekly upcoming items, including saved snapshots', () => { const owner = 'closed-feed@example.test'; const base = { tarih:'07.09.2026', saat:'10:00' }; const closed = ['İptal', 'Sonuçlandı', 'Sözleşme İmzalandı'].map((durum, i) => ({ ...base, id:`closed-${i}`, ikn:`2026/closed-${i}`, durum })); closed.push({ ...base, id:'winner', ikn:'2026/winner', kazananFirma:'Test kazananı' }); const open = { ...base, id:'open', ikn:'2026/open', durum:'Açık' }; const unknownTime = { ...open, id:'unknown-time', ikn:'2026/unknown-time', saat:'' }; const all = [...closed, open, unknownTime]; for (const tender of all) store.saveTenderState(owner, tender.ikn, { takipte:true }, tenderSnapshot(tender), 0); assert.deepEqual(ihaleFiltrele(all, {}, now).map(t => t.id), ['open', 'unknown-time']); assert.equal(ihaleFiltrele(all, { sadeceAcik:false }, now).length, all.length); // Status updates in the current source must override a previously open saved snapshot. const updated = { ...open, durum:'İptal' }; const upcoming = yaklasanIhaleler(owner, [updated, unknownTime], 14, now); assert.deepEqual(upcoming.map(t => t.ikn), ['2026/unknown-time']); assert.equal(upcoming[0].saat_dogrulanmali, true); for (const output of [briefMetni, briefHtml]) { const body = output({ hafta:'Test haftası', yaklasan:upcoming, sonuclar:[] }); for (const tender of [...closed, open]) assert.ok(!body.includes(tender.ikn)); assert.match(body, /saat doğrulanmalı/); } });