"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { InMemoryDashboardStore, MySqlDashboardStore, createDashboardStore, } = require("../src/dashboardStore"); async function withPatchedEnv(patch, fn) { const backup = {}; for (const [key] of Object.entries(patch)) { backup[key] = process.env[key]; } for (const [key, value] of Object.entries(patch)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } try { return await fn(); } finally { for (const [key, value] of Object.entries(backup)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } } } test("in-memory store persists watch, poll result, events and controls", async () => { const store = new InMemoryDashboardStore(); await store.init(); await store.saveWatch({ id: "watch-1", rawInput: "인천->마드리드", searchParams: { segments: [{ from: "ICN", to: "MAD" }] }, alertRules: { targetPrice: 1300000, notifyOnPriceChange: true }, pollingEnabled: true, alertsEnabled: true, createdAt: "2026-02-19T00:00:00.000Z", updatedAt: "2026-02-19T00:00:00.000Z", lastSnapshot: null, lastError: null, }); await store.savePollResult("watch-1", { snapshot: { polledAt: "2026-02-19T00:01:00.000Z", bestPrice: 1295000, currency: "KRW", bestOffer: { provider: "mock" }, offers: [{ provider: "mock", price: 1295000, currency: "KRW" }], }, }); await store.saveEvent({ watchId: "watch-1", eventType: "target_price", observedAt: "2026-02-19T00:01:00.000Z", payload: { currentBestPrice: 1295000, previousBestPrice: 1310000, currency: "KRW", }, }); const watches = await store.listWatches(); const events = await store.listEvents(5); assert.equal(watches.length, 1); assert.equal(watches[0].lastSnapshot.bestPrice, 1295000); assert.equal(events.length, 1); assert.equal(events[0].eventType, "target_price"); const controlsBefore = await store.getGlobalControls(); assert.equal(controlsBefore.crawlingEnabled, true); assert.equal(controlsBefore.alertsEnabled, true); await store.setGlobalControls({ crawlingEnabled: false, alertsEnabled: false, }); const controlsAfter = await store.getGlobalControls(); assert.equal(controlsAfter.crawlingEnabled, false); assert.equal(controlsAfter.alertsEnabled, false); }); test("mysql mode fails closed without fallback when configuration is missing", async () => { await withPatchedEnv( { DASHBOARD_DB: undefined, MYSQL_URL: undefined, MYSQL_HOST: undefined, MYSQL_USER: undefined, MYSQL_DATABASE: undefined, DASHBOARD_ALLOW_MEMORY_FALLBACK: undefined, }, async () => { await assert.rejects( () => createDashboardStore({ mode: "mysql", allowMemoryFallback: false, }), /MySQL 연결 정보가 필요합니다/ ); } ); }); test("mysql mode can fallback to memory only when explicitly allowed", async () => { await withPatchedEnv( { DASHBOARD_DB: undefined, MYSQL_URL: undefined, MYSQL_HOST: undefined, MYSQL_USER: undefined, MYSQL_DATABASE: undefined, DASHBOARD_ALLOW_MEMORY_FALLBACK: undefined, }, async () => { const setup = await createDashboardStore({ mode: "mysql", allowMemoryFallback: true, }); assert.equal(setup.engine, "memory"); assert.match(setup.warning, /메모리 저장소/); await setup.store.close(); } ); }); test("mysql store init fails when required tables are missing", async () => { const calls = []; const store = new MySqlDashboardStore( { query: async (sql) => { calls.push(sql); return [[{ TABLE_NAME: "projects" }], []]; }, end: async () => {}, } ); await assert.rejects( () => store.init(), /MySQL playground 스키마가 준비되지 않았습니다\..*project_documents.*project_events.*project_settings/ ); assert.equal(calls.length, 1); }); test("mysql store init succeeds when all required tables exist", async () => { const calls = []; const store = new MySqlDashboardStore( { query: async (sql, params) => { calls.push(sql); if (String(sql).includes("FROM information_schema.TABLES")) { return [ [ { TABLE_NAME: "projects" }, { TABLE_NAME: "project_documents" }, { TABLE_NAME: "project_events" }, { TABLE_NAME: "project_settings" }, ], [], ]; } if (String(sql).includes("INSERT INTO projects")) { assert.equal(params[0], "air-watcher"); return [{ affectedRows: 1 }, []]; } throw new Error(`unexpected query: ${sql}`); }, end: async () => {}, } ); await assert.doesNotReject(() => store.init()); assert.equal(store.schemaMode, "playground"); assert.equal(calls.length, 2); }); test("mysql store supports custom projectKey", async () => { let insertedProjectKey = null; const store = new MySqlDashboardStore({ query: async (sql, params) => { if (String(sql).includes("FROM information_schema.TABLES")) { return [ [ { TABLE_NAME: "projects" }, { TABLE_NAME: "project_documents" }, { TABLE_NAME: "project_events" }, { TABLE_NAME: "project_settings" }, ], [], ]; } if (String(sql).includes("INSERT INTO projects")) { insertedProjectKey = params[0]; return [{ affectedRows: 1 }, []]; } throw new Error(`unexpected query: ${sql}`); }, end: async () => {}, }, { projectKey: "mini-app-a", }); await store.init(); assert.equal(insertedProjectKey, "mini-app-a"); });