Files
Cibello-app/apps/api/test/ops.test.ts
T
Sven (AAMOS AI) 4687f46384 Phase 1 ops core: base blocks, feedback, canary fixes
- Add app/ekonomi/anvandare/prenumerationer/butik/feedback blocks to computeOpsSummary.
- New feedback table, POST /api/feedback, GDPR erasure wiring.
- Canary: only count unverified/rejected published recipes; treat verified/editorial as safe.
- Add 'rejected' recipe verification status enum value.
- Fix food-safety lint baseline: remove egg/cured fish from raw-protein list, relax
  genomstekt regex, add safe-cooking phrases to seed recipes.
- Seed recipes now get verificationStatus=editorial.
- Tests: ops summary blocks, feedback route, me.residual cleanup.
2026-08-10 06:35:51 +07:00

208 lines
6.5 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/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);
});
});