Files
air-watcher/test/dashboardStore.test.js
2026-03-05 11:00:45 +09:00

224 lines
5.9 KiB
JavaScript

"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");
});