224 lines
5.9 KiB
JavaScript
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");
|
|
});
|