Files
Cibello-app/apps/api/test/ops.test.ts
T
2026-08-11 07:37:51 +07:00

238 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ReturnType<typeof buildServer>>;
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<ReturnType<typeof buildServer>>;
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<unknown> };
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<number>`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<number>`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);
});
});