import "./setup-env.js"; import { describe, expect, it, beforeAll, afterAll } from "vitest"; import { buildServer } from "../src/server.js"; import { loadConfig } from "../src/config.js"; import { sql } from "drizzle-orm"; import { createDatabase, schema } from "@app/database"; import { computeOpsSummary, type OpsQueueSummary } from "@app/database/ops-summary"; const testDb = createDatabase(process.env.TEST_DATABASE_URL!); const config = loadConfig(); const queueSummary: OpsQueueSummary = { vantande: 2, aktiva: 1, misslyckade_24h: 0, aldsta_vantande_sek: null, workers_ok: true, }; const planPrices = { free: 0, household: 9900, family: 14900, large_household: 19900, }; describe("/ops/v1/health", () => { let app: Awaited>; beforeAll(async () => { app = await buildServer(config); await app.ready(); }); afterAll(async () => { await app.close(); }); it("avvisar anrop utan token", async () => { const res = await app.inject({ method: "GET", url: "/ops/v1/health" }); expect(res.statusCode).toBe(401); }); it("returnerar ok med app-namn vid giltig token", async () => { const res = await app.inject({ method: "GET", url: "/ops/v1/health", headers: { authorization: `Bearer ${config.OPS_TOKEN}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body) as { ok: boolean; app: string }; expect(body.ok).toBe(true); expect(body.app).toBe("cibello"); }); }); describe("/ops/v1/summary", () => { let app: Awaited>; beforeAll(async () => { app = await buildServer(config); await app.ready(); await cleanup(); }); afterAll(async () => { await cleanup(); await app.close(); await testDb.pool.end(); }); async function cleanup() { await testDb.db.delete(schema.opsSafetyCanary); await testDb.db.delete(schema.productAnalyticsEvents); await testDb.db.delete(schema.feedback); await app.redis.del("ops:summary:cache", "ops:summary:computed_at"); } it("avvisar anrop utan token", async () => { const res = await app.inject({ method: "GET", url: "/ops/v1/summary" }); expect(res.statusCode).toBe(401); }); it("avvisar felaktig token", async () => { const res = await app.inject({ method: "GET", url: "/ops/v1/summary", headers: { authorization: "Bearer wrong-token" }, }); expect(res.statusCode).toBe(401); }); it("returnerar 503 när cachen saknas", async () => { await app.redis.del("ops:summary:cache", "ops:summary:computed_at"); const res = await app.inject({ method: "GET", url: "/ops/v1/summary", headers: { authorization: `Bearer ${config.OPS_TOKEN}` }, }); expect(res.statusCode).toBe(503); const body = JSON.parse(res.body) as { error: { code: string } }; expect(body.error.code).toBe("CACHE_MISS"); }); it("serverar cachad summary med rätt block", async () => { const now = new Date(); await testDb.db.insert(schema.productAnalyticsEvents).values([ { eventName: "scan_started", occurredAt: now, properties: {}, userId: null }, { eventName: "scan_completed", occurredAt: now, properties: { latencyMs: 120 }, userId: null, }, { eventName: "scan_completed", occurredAt: now, properties: { latencyMs: 240 }, userId: null, }, { eventName: "cooking_session_completed", occurredAt: now, properties: {}, userId: null, }, { eventName: "account_created", occurredAt: now, properties: {}, userId: null, }, { eventName: "account_created", occurredAt: now, properties: {}, userId: null, }, { eventName: "recommendations_viewed", occurredAt: now, properties: {}, userId: null, }, { eventName: "recommendation_opened", occurredAt: now, properties: {}, userId: null, }, ]); await testDb.db.insert(schema.opsSafetyCanary).values({ allergenInvariantBrott: 0, overifieradeVisade: 0, foodSafetyLintAvvisade7d: 0, }); await testDb.db.insert(schema.feedback).values([ { userId: null, typ: "bug", rubrik: "Knappen fungerar inte", text: "Detaljerad text – ska aldrig visas i summary.", status: "oppen", }, { userId: null, typ: "onskemal", rubrik: "Mörkt läge", text: "Önskemålstext – ska aldrig visas i summary.", status: "stangd", }, ]); const summary = await computeOpsSummary({ db: testDb.db, budgetUsd: 0, queueSummary, planPrices, }); await app.redis.set("ops:summary:cache", JSON.stringify(summary), "EX", 60); await app.redis.set("ops:summary:computed_at", summary.as_of, "EX", 60); const res = await app.inject({ method: "GET", url: "/ops/v1/summary", headers: { authorization: `Bearer ${config.OPS_TOKEN}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body) as { app: { app: string; generated_at: string }; ekonomi: { mrr: number | null; intakt_24h: number | null; valuta: string }; anvandare: { aktiva_nu: number; dau: number; mau: number; nya_24h: number; nya_7d: number }; prenumerationer: { trial_aktiva: number; betalande: number }; butik: { butik: null }; feedback: { oppna: number; nya_24h: number; senaste: Array }; ai_scan: { scans_24h: number; latens_p50_ms: number | null }; engagemang: { lagade_maltider_24h: number }; sakerhet: { overifierade_visade: number; senaste_kontroll: string | null }; jobb: { vantande: number; workers_ok: boolean }; cached_at: string; }; expect(body.app.app).toBe("cibello"); expect(body.app.generated_at).toBeTruthy(); expect(body.ekonomi.valuta).toBe("SEK"); expect(body.anvandare.nya_24h).toBe(2); expect(body.anvandare.dau).toBe(0); const payingNow = await testDb.db .select({ count: sql`count(*)::int` }) .from(schema.subscriptions) .where( sql`${schema.subscriptions.status} = 'active' AND (${schema.subscriptions.expiresAt} IS NULL OR ${schema.subscriptions.expiresAt} > now())`, ); expect(body.prenumerationer.betalande).toBe(Number(payingNow[0]?.count ?? 0)); const trialsNow = await testDb.db .select({ count: sql`count(*)::int` }) .from(schema.trials) .where(sql`${schema.trials.endsAt} >= now()`); expect(body.prenumerationer.trial_aktiva).toBe(Number(trialsNow[0]?.count ?? 0)); expect(body.butik.butik).toBeNull(); expect(body.feedback.oppna).toBe(1); expect(body.feedback.nya_24h).toBe(2); expect(body.feedback.senaste.length).toBe(2); expect(body.feedback.senaste[0]).not.toHaveProperty("text"); expect(body.ai_scan.scans_24h).toBe(2); expect(body.ai_scan.latens_p50_ms).toBeGreaterThan(0); expect(body.engagemang.lagade_maltider_24h).toBe(1); expect(body.sakerhet.overifierade_visade).toBe(0); expect(body.sakerhet.senaste_kontroll).toBeTruthy(); expect(body.jobb.vantande).toBe(2); expect(body.jobb.workers_ok).toBe(true); expect(body.cached_at).toBe(summary.as_of); }); });