Fas 3 steg 3a-fix: hermetisk test-setup + analytics-event regler för cooking sessions
This commit is contained in:
@@ -58,19 +58,22 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
|
||||
})
|
||||
.returning();
|
||||
|
||||
await trackProductAnalytics(
|
||||
app.db,
|
||||
req.userId,
|
||||
cookingSessionStarted({
|
||||
householdId,
|
||||
properties: {
|
||||
cookingSessionId: session!.id,
|
||||
recipeId: id,
|
||||
status,
|
||||
plannedPortions: input.portions ?? recipe.portions,
|
||||
},
|
||||
}),
|
||||
);
|
||||
// cooking_session_started skickas endast vid faktisk start, inte vid planned.
|
||||
if (status === "started") {
|
||||
await trackProductAnalytics(
|
||||
app.db,
|
||||
req.userId,
|
||||
cookingSessionStarted({
|
||||
householdId,
|
||||
properties: {
|
||||
cookingSessionId: session!.id,
|
||||
recipeId: id,
|
||||
status,
|
||||
plannedPortions: input.portions ?? recipe.portions,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return reply.status(201).send({ session: session });
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ describe("cooking sessions", () => {
|
||||
const config = loadConfig();
|
||||
let app: Awaited<ReturnType<typeof buildServer>>;
|
||||
let token: string;
|
||||
let userId: string;
|
||||
let householdId: string;
|
||||
let recipeId: string;
|
||||
const email = "cooking-session-test@example.invalid";
|
||||
@@ -46,6 +47,7 @@ describe("cooking sessions", () => {
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, m.householdId));
|
||||
}
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.productAnalyticsEvents).where(eq(schema.productAnalyticsEvents.userId, u.id));
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
}
|
||||
}
|
||||
@@ -61,6 +63,8 @@ describe("cooking sessions", () => {
|
||||
payload: { email, password: "Password123!", displayName: "Cooking Test" },
|
||||
});
|
||||
token = (JSON.parse(res.body) as { accessToken: string }).accessToken;
|
||||
const profile = await app.inject({ method: "GET", url: "/v1/me", headers: { authorization: `Bearer ${token}` } });
|
||||
userId = (JSON.parse(profile.body) as { id: string }).id;
|
||||
|
||||
const quick = await app.inject({
|
||||
method: "POST",
|
||||
@@ -85,7 +89,7 @@ describe("cooking sessions", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("creates a planned session", async () => {
|
||||
it("creates a planned session without firing cooking_session_started", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
@@ -96,9 +100,15 @@ describe("cooking sessions", () => {
|
||||
const body = JSON.parse(res.body) as { session: { status: string; plannedPortions: number } };
|
||||
expect(body.session.status).toBe("planned");
|
||||
expect(body.session.plannedPortions).toBe(4);
|
||||
|
||||
const events = await testDb.db
|
||||
.select({ name: schema.productAnalyticsEvents.eventName })
|
||||
.from(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, userId));
|
||||
expect(events.filter((e) => e.name === "cooking_session_started").length).toBe(0);
|
||||
});
|
||||
|
||||
it("starts a planned session", async () => {
|
||||
it("starts a planned session and fires cooking_session_started", async () => {
|
||||
const start = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
@@ -116,6 +126,13 @@ describe("cooking sessions", () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { session: { status: string } };
|
||||
expect(body.session.status).toBe("started");
|
||||
|
||||
const started = await testDb.db
|
||||
.select()
|
||||
.from(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, userId))
|
||||
.orderBy(schema.productAnalyticsEvents.occurredAt);
|
||||
expect(started.filter((e) => e.eventName === "cooking_session_started").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("cancels a session without touching inventory", async () => {
|
||||
@@ -217,6 +234,47 @@ describe("cooking sessions", () => {
|
||||
expect(body.mealBoxId).toBeDefined();
|
||||
});
|
||||
|
||||
it("emits cooking_session_completed on complete and cooking_session_cancelled on cancel", async () => {
|
||||
// complete
|
||||
const startComplete = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { startNow: true },
|
||||
});
|
||||
const completeId = (JSON.parse(startComplete.body) as { session: { id: string } }).session.id;
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/cooking-sessions/${completeId}/complete`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { mealBoxPortions: 0, deductInventory: true },
|
||||
});
|
||||
|
||||
// cancel
|
||||
const startCancel = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { startNow: true },
|
||||
});
|
||||
const cancelId = (JSON.parse(startCancel.body) as { session: { id: string } }).session.id;
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/cooking-sessions/${cancelId}/cancel`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { reason: "test" },
|
||||
});
|
||||
|
||||
const names = (
|
||||
await testDb.db
|
||||
.select({ name: schema.productAnalyticsEvents.eventName })
|
||||
.from(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, userId))
|
||||
).map((r) => r.name);
|
||||
expect(names).toContain("cooking_session_completed");
|
||||
expect(names).toContain("cooking_session_cancelled");
|
||||
});
|
||||
|
||||
it("times out started sessions older than 24 h", async () => {
|
||||
const start = await app.inject({
|
||||
method: "POST",
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
---
|
||||
|
||||
## Hermetiska testregler
|
||||
|
||||
Alla integrationstester ska vara hermetiska:
|
||||
- De får inte bero på innehållet i `.env` (värden som `AAMOS_MODE`, `EMAIL_MODE`, `S3_MODE` skrivs över i `setup-env.ts`).
|
||||
- De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
|
||||
- De får inte bero på databasinnehåll som skapats utanför testet (exempelvis seedade recept från en tidigare körning). Testkörningen ansvarar själv för att testdatabasen är migrerad **och** seedad innan testerna startar; seeden är idempotent.
|
||||
- Körning: `pnpm --filter @app/database run db:test-setup` (migrate + seed) ska alltid föregå API-testerna.
|
||||
|
||||
## 1. REUSE/GAP-matris mot befintligt matlagningsflöde
|
||||
|
||||
| §6-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "tsx src/migrate.ts --test && vitest run --passWithNoTests",
|
||||
"test": "pnpm run db:test-setup && vitest run --passWithNoTests",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "tsx src/migrate.ts",
|
||||
"db:seed": "tsx src/seed/run.ts"
|
||||
"db:seed": "tsx src/seed/run.ts",
|
||||
"db:test-migrate": "tsx src/migrate.ts --test",
|
||||
"db:test-seed": "tsx src/seed/run.ts --test",
|
||||
"db:test-setup": "tsx src/migrate.ts --test && tsx src/seed/run.ts --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@app/nutrition-engine": "workspace:*",
|
||||
|
||||
+16
@@ -10,11 +10,27 @@
|
||||
"dependsOn": ["^typecheck"],
|
||||
"outputs": []
|
||||
},
|
||||
"db:test-setup": {
|
||||
"dependsOn": [],
|
||||
"outputs": [],
|
||||
"cache": false,
|
||||
"env": ["TEST_DATABASE_URL"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^typecheck"],
|
||||
"outputs": [],
|
||||
"env": ["TEST_DATABASE_URL"]
|
||||
},
|
||||
"@app/api#test": {
|
||||
"dependsOn": ["@app/database#db:test-setup"],
|
||||
"outputs": [],
|
||||
"env": ["TEST_DATABASE_URL"]
|
||||
},
|
||||
"@app/database#test": {
|
||||
"dependsOn": ["@app/database#db:test-setup"],
|
||||
"outputs": [],
|
||||
"env": ["TEST_DATABASE_URL"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
|
||||
Reference in New Issue
Block a user