ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s
CI / Typecheck, test & build (push) Failing after 2s
This commit is contained in:
@@ -19,18 +19,29 @@ describe("admin household trust score", () => {
|
||||
.from(schema.users)
|
||||
.where(inArray(schema.users.email, [adminEmail]));
|
||||
for (const u of existing) {
|
||||
await testDb.db.delete(schema.inventoryTransactions).where(eq(schema.inventoryTransactions.actorUserId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.actorUserId, u.id));
|
||||
const owned = await testDb.db
|
||||
.select({ id: schema.households.id })
|
||||
.from(schema.households)
|
||||
.innerJoin(schema.householdMembers, eq(schema.householdMembers.householdId, schema.households.id))
|
||||
.innerJoin(
|
||||
schema.householdMembers,
|
||||
eq(schema.householdMembers.householdId, schema.households.id),
|
||||
)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
for (const h of owned) {
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, h.id));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, h.id));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, h.id));
|
||||
}
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
}
|
||||
@@ -74,7 +85,12 @@ describe("admin household trust score", () => {
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { householdId: string; score: number; status: string; itemCount: number };
|
||||
const body = JSON.parse(res.body) as {
|
||||
householdId: string;
|
||||
score: number;
|
||||
status: string;
|
||||
itemCount: number;
|
||||
};
|
||||
expect(body.householdId).toBe(householdId);
|
||||
expect(typeof body.score).toBe("number");
|
||||
expect(["up_to_date", "needs_check", "uncertain"]).toContain(body.status);
|
||||
|
||||
@@ -18,7 +18,12 @@ describe("cooking sessions", () => {
|
||||
let recipeId: string;
|
||||
const email = "cooking-session-test@example.invalid";
|
||||
|
||||
async function createItemWithPurchase(canonicalIngredientId: string, displayName: string, quantity: number, unit: Unit) {
|
||||
async function createItemWithPurchase(
|
||||
canonicalIngredientId: string,
|
||||
displayName: string,
|
||||
quantity: number,
|
||||
unit: Unit,
|
||||
) {
|
||||
const [location] = await testDb.db
|
||||
.select({ id: schema.storageLocations.id })
|
||||
.from(schema.storageLocations)
|
||||
@@ -64,26 +69,46 @@ describe("cooking sessions", () => {
|
||||
.where(eq(schema.inventoryTransactions.cookingSessionId, s.id));
|
||||
await testDb.db.delete(schema.meals).where(eq(schema.meals.cookingSessionId, s.id));
|
||||
await testDb.db.delete(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, s.id));
|
||||
await testDb.db.delete(schema.recipeCooks).where(eq(schema.recipeCooks.cookingSessionId, s.id));
|
||||
await testDb.db
|
||||
.delete(schema.recipeCooks)
|
||||
.where(eq(schema.recipeCooks.cookingSessionId, s.id));
|
||||
}
|
||||
await testDb.db.delete(schema.cookingSessions).where(eq(schema.cookingSessions.startedByUserId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.cookingSessions)
|
||||
.where(eq(schema.cookingSessions.startedByUserId, u.id));
|
||||
const memberships = await testDb.db
|
||||
.select({ householdId: schema.householdMembers.householdId })
|
||||
.from(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
for (const m of memberships) {
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.mealBoxes).where(eq(schema.mealBoxes.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.meals).where(eq(schema.meals.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.recipeCooks).where(eq(schema.recipeCooks.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.cookingSessions).where(eq(schema.cookingSessions.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.recipeCooks)
|
||||
.where(eq(schema.recipeCooks.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.cookingSessions)
|
||||
.where(eq(schema.cookingSessions.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.cookingAssumptionProfiles)
|
||||
.where(eq(schema.cookingAssumptionProfiles.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
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.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, u.id));
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
}
|
||||
}
|
||||
@@ -99,7 +124,11 @@ 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}` } });
|
||||
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({
|
||||
@@ -168,7 +197,9 @@ describe("cooking sessions", () => {
|
||||
.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);
|
||||
expect(
|
||||
started.filter((e) => e.eventName === "cooking_session_started").length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("cancels a session without touching inventory", async () => {
|
||||
@@ -183,7 +214,10 @@ describe("cooking sessions", () => {
|
||||
const before = await testDb.db
|
||||
.select({ count: count(schema.inventoryTransactions.id) })
|
||||
.from(schema.inventoryTransactions)
|
||||
.innerJoin(schema.inventoryItems, eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id))
|
||||
.innerJoin(
|
||||
schema.inventoryItems,
|
||||
eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id),
|
||||
)
|
||||
.where(eq(schema.inventoryItems.householdId, householdId));
|
||||
|
||||
const res = await app.inject({
|
||||
@@ -199,7 +233,10 @@ describe("cooking sessions", () => {
|
||||
const after = await testDb.db
|
||||
.select({ count: count(schema.inventoryTransactions.id) })
|
||||
.from(schema.inventoryTransactions)
|
||||
.innerJoin(schema.inventoryItems, eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id))
|
||||
.innerJoin(
|
||||
schema.inventoryItems,
|
||||
eq(schema.inventoryTransactions.inventoryItemId, schema.inventoryItems.id),
|
||||
)
|
||||
.where(eq(schema.inventoryItems.householdId, householdId));
|
||||
expect(after[0]!.count).toBe(before[0]!.count);
|
||||
});
|
||||
@@ -248,7 +285,11 @@ describe("cooking sessions", () => {
|
||||
.where(eq(schema.inventoryItems.id, d.itemId))
|
||||
.limit(1);
|
||||
const txs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, d.itemId));
|
||||
const balance = computeBalance(txs);
|
||||
@@ -264,7 +305,11 @@ describe("cooking sessions", () => {
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 2, deductInventory: true },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { ok: boolean; sessionId: string; mealBoxId: string | null };
|
||||
const body = JSON.parse(res.body) as {
|
||||
ok: boolean;
|
||||
sessionId: string;
|
||||
mealBoxId: string | null;
|
||||
};
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.sessionId).toBeDefined();
|
||||
expect(body.mealBoxId).toBeDefined();
|
||||
@@ -298,14 +343,20 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
|
||||
it("legacy /cook updates cooking assumption profiles", async () => {
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
await testDb.db
|
||||
.delete(schema.cookingAssumptionProfiles)
|
||||
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
|
||||
const recipe = (await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptionalIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
|
||||
const recipe = (
|
||||
await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptionalIngredientId = recipe.ingredients.find(
|
||||
(i) => i.canonicalIngredientId && !i.optional,
|
||||
)?.canonicalIngredientId;
|
||||
expect(firstNonOptionalIngredientId).toBeDefined();
|
||||
|
||||
await app.inject({
|
||||
@@ -338,7 +389,10 @@ describe("cooking sessions", () => {
|
||||
|
||||
it("legacy /cook writes cooking_session_started and cooking_session_completed analytics", async () => {
|
||||
const before = await testDb.db
|
||||
.select({ id: schema.productAnalyticsEvents.id, eventName: schema.productAnalyticsEvents.eventName })
|
||||
.select({
|
||||
id: schema.productAnalyticsEvents.id,
|
||||
eventName: schema.productAnalyticsEvents.eventName,
|
||||
})
|
||||
.from(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, userId));
|
||||
const beforeIds = new Set(before.map((e) => e.id));
|
||||
@@ -357,9 +411,14 @@ describe("cooking sessions", () => {
|
||||
.from(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, userId));
|
||||
const newEvents = after.filter((e) => !beforeIds.has(e.id));
|
||||
const props = (e: (typeof after)[number]) => (e.properties ?? {}) as { cookingSessionId?: string };
|
||||
const started = newEvents.filter((e) => e.eventName === "cooking_session_started" && props(e).cookingSessionId === sessionId);
|
||||
const completed = newEvents.filter((e) => e.eventName === "cooking_session_completed" && props(e).cookingSessionId === sessionId);
|
||||
const props = (e: (typeof after)[number]) =>
|
||||
(e.properties ?? {}) as { cookingSessionId?: string };
|
||||
const started = newEvents.filter(
|
||||
(e) => e.eventName === "cooking_session_started" && props(e).cookingSessionId === sessionId,
|
||||
);
|
||||
const completed = newEvents.filter(
|
||||
(e) => e.eventName === "cooking_session_completed" && props(e).cookingSessionId === sessionId,
|
||||
);
|
||||
expect(started.length).toBe(1);
|
||||
expect(completed.length).toBe(1);
|
||||
});
|
||||
@@ -395,7 +454,9 @@ describe("cooking sessions", () => {
|
||||
payload: { mealBoxPortions: 1, actualPortionsEaten: 2, leftoverEstimatePortions: 1 },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { session: { actualPortionsEaten: number; leftoverEstimatePortions: number } };
|
||||
const body = JSON.parse(res.body) as {
|
||||
session: { actualPortionsEaten: number; leftoverEstimatePortions: number };
|
||||
};
|
||||
expect(body.session.actualPortionsEaten).toBe(2);
|
||||
expect(body.session.leftoverEstimatePortions).toBe(1);
|
||||
});
|
||||
@@ -419,7 +480,9 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
|
||||
it("updates cooking assumption profiles per household and ingredient", async () => {
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
await testDb.db
|
||||
.delete(schema.cookingAssumptionProfiles)
|
||||
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
|
||||
const start = await app.inject({
|
||||
method: "POST",
|
||||
@@ -429,12 +492,16 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
const sessionId = (JSON.parse(start.body) as { session: { id: string } }).session.id;
|
||||
|
||||
const recipe = (await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptionalIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
|
||||
const recipe = (
|
||||
await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptionalIngredientId = recipe.ingredients.find(
|
||||
(i) => i.canonicalIngredientId && !i.optional,
|
||||
)?.canonicalIngredientId;
|
||||
expect(firstNonOptionalIngredientId).toBeDefined();
|
||||
|
||||
await app.inject({
|
||||
@@ -461,7 +528,9 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
|
||||
it("cooking-assumptions ignores optional first ingredient and returns defaults from a non-optional one", async () => {
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
await testDb.db
|
||||
.delete(schema.cookingAssumptionProfiles)
|
||||
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
|
||||
// Skapa ett recept där den första ingrediensen är valfri.
|
||||
const [recipe] = await testDb.db
|
||||
@@ -480,7 +549,16 @@ describe("cooking sessions", () => {
|
||||
cookTimeMinutes: 10,
|
||||
totalTimeMinutes: 15,
|
||||
portions: 4,
|
||||
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 100,
|
||||
proteinG: 5,
|
||||
fatG: 3,
|
||||
carbsG: 12,
|
||||
saturatedFatG: 1,
|
||||
fiberG: 1,
|
||||
sugarG: 2,
|
||||
saltG: 0.1,
|
||||
},
|
||||
allergens: [],
|
||||
spiceLevel: 0,
|
||||
dna: {
|
||||
@@ -554,7 +632,9 @@ describe("cooking sessions", () => {
|
||||
expect(body.defaultLeftoverEstimatePortions).toBe(1);
|
||||
|
||||
// Städa upp testreceptet.
|
||||
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db
|
||||
.delete(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||
});
|
||||
|
||||
@@ -619,12 +699,16 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
|
||||
it("undo restores inventory, removes meals, discards meal boxes and deletes recipe_cooks", async () => {
|
||||
const recipe = (await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptional = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
|
||||
const recipe = (
|
||||
await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptional = recipe.ingredients.find(
|
||||
(i) => i.canonicalIngredientId && !i.optional,
|
||||
)?.canonicalIngredientId;
|
||||
|
||||
// Sätt upp ett känt lager om receptet har en icke-valfri ingrediens.
|
||||
let itemId: string | undefined;
|
||||
@@ -660,7 +744,11 @@ describe("cooking sessions", () => {
|
||||
payload: {},
|
||||
});
|
||||
expect(undo.statusCode).toBe(200);
|
||||
const undoBody = JSON.parse(undo.body) as { ok: boolean; removedMealIds: string[]; discardedMealBoxIds: string[] };
|
||||
const undoBody = JSON.parse(undo.body) as {
|
||||
ok: boolean;
|
||||
removedMealIds: string[];
|
||||
discardedMealBoxIds: string[];
|
||||
};
|
||||
expect(undoBody.ok).toBe(true);
|
||||
|
||||
// Sessionstatus = undone.
|
||||
@@ -679,16 +767,25 @@ describe("cooking sessions", () => {
|
||||
expect(Number(txsAfter[0]!.count)).toBeGreaterThan(Number(txsBefore[0]!.count));
|
||||
|
||||
// Meals borttagna.
|
||||
const meals = await testDb.db.select({ count: count(schema.meals.id) }).from(schema.meals).where(eq(schema.meals.cookingSessionId, sessionId));
|
||||
const meals = await testDb.db
|
||||
.select({ count: count(schema.meals.id) })
|
||||
.from(schema.meals)
|
||||
.where(eq(schema.meals.cookingSessionId, sessionId));
|
||||
expect(Number(meals[0]!.count)).toBe(0);
|
||||
|
||||
// Matlådor markerade discarded.
|
||||
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
const boxes = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
expect(boxes.length).toBe(undoBody.discardedMealBoxIds.length);
|
||||
for (const box of boxes) expect(box.status).toBe("discarded");
|
||||
|
||||
// recipe_cooks borttagen och cookCount backad.
|
||||
const cooks = await testDb.db.select().from(schema.recipeCooks).where(eq(schema.recipeCooks.cookingSessionId, sessionId));
|
||||
const cooks = await testDb.db
|
||||
.select()
|
||||
.from(schema.recipeCooks)
|
||||
.where(eq(schema.recipeCooks.cookingSessionId, sessionId));
|
||||
expect(cooks.length).toBe(0);
|
||||
|
||||
// Inventory-transaktionsinvariant.
|
||||
@@ -699,7 +796,11 @@ describe("cooking sessions", () => {
|
||||
.where(eq(schema.inventoryItems.id, itemId))
|
||||
.limit(1);
|
||||
const itemTxs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(itemTxs);
|
||||
@@ -712,13 +813,16 @@ describe("cooking sessions", () => {
|
||||
.from(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, userId))
|
||||
.orderBy(schema.productAnalyticsEvents.occurredAt);
|
||||
const props = (e: (typeof analytics)[number]) => (e.properties ?? {}) as { cookingSessionId?: string };
|
||||
expect(analytics.some((e) => e.eventName === "cooking_session_undone" && props(e).cookingSessionId === sessionId)).toBe(true);
|
||||
const props = (e: (typeof analytics)[number]) =>
|
||||
(e.properties ?? {}) as { cookingSessionId?: string };
|
||||
expect(
|
||||
analytics.some(
|
||||
(e) => e.eventName === "cooking_session_undone" && props(e).cookingSessionId === sessionId,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
if (itemId) {
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.id, itemId));
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.id, itemId));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -771,10 +875,28 @@ describe("cooking sessions", () => {
|
||||
cookTimeMinutes: 10,
|
||||
totalTimeMinutes: 15,
|
||||
portions: 4,
|
||||
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 100,
|
||||
proteinG: 5,
|
||||
fatG: 3,
|
||||
carbsG: 12,
|
||||
saturatedFatG: 1,
|
||||
fiberG: 1,
|
||||
sugarG: 2,
|
||||
saltG: 0.1,
|
||||
},
|
||||
allergens: [],
|
||||
spiceLevel: 0,
|
||||
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
|
||||
dna: {
|
||||
cuisine: "international",
|
||||
vegetables: [],
|
||||
flavorProfile: [],
|
||||
spiceLevel: 0,
|
||||
method: "stovetop",
|
||||
timeMinutes: 15,
|
||||
calories: 100,
|
||||
proteinGrams: 5,
|
||||
},
|
||||
status: "published",
|
||||
verificationStatus: "unverified",
|
||||
sourceType: "own_editorial",
|
||||
@@ -794,7 +916,12 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry")));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry"),
|
||||
),
|
||||
);
|
||||
const itemId = await createItemWithPurchase("pasta_dry", "Pasta", 400, "GRAM");
|
||||
|
||||
const start = await app.inject({
|
||||
@@ -838,8 +965,15 @@ describe("cooking sessions", () => {
|
||||
// Städa testreceptet och lagerposten.
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry")));
|
||||
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "pasta_dry"),
|
||||
),
|
||||
);
|
||||
await testDb.db
|
||||
.delete(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||
});
|
||||
|
||||
@@ -879,10 +1013,28 @@ describe("cooking sessions", () => {
|
||||
cookTimeMinutes: 10,
|
||||
totalTimeMinutes: 15,
|
||||
portions: 4,
|
||||
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 100,
|
||||
proteinG: 5,
|
||||
fatG: 3,
|
||||
carbsG: 12,
|
||||
saturatedFatG: 1,
|
||||
fiberG: 1,
|
||||
sugarG: 2,
|
||||
saltG: 0.1,
|
||||
},
|
||||
allergens: [],
|
||||
spiceLevel: 0,
|
||||
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
|
||||
dna: {
|
||||
cuisine: "international",
|
||||
vegetables: [],
|
||||
flavorProfile: [],
|
||||
spiceLevel: 0,
|
||||
method: "stovetop",
|
||||
timeMinutes: 15,
|
||||
calories: 100,
|
||||
proteinGrams: 5,
|
||||
},
|
||||
status: "published",
|
||||
verificationStatus: "unverified",
|
||||
sourceType: "own_editorial",
|
||||
@@ -902,7 +1054,12 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast")));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast"),
|
||||
),
|
||||
);
|
||||
const itemId = await createItemWithPurchase("chicken_breast", "Kyckling", 400, "GRAM");
|
||||
|
||||
const start = await app.inject({
|
||||
@@ -930,13 +1087,20 @@ describe("cooking sessions", () => {
|
||||
expect(item[0]!.quantity).toBeCloseTo(0, 1);
|
||||
|
||||
// Matlådan ska ha 2 portioner.
|
||||
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
const boxes = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
expect(boxes.length).toBe(1);
|
||||
expect(boxes[0]!.portions).toBe(2);
|
||||
|
||||
// Invariant.
|
||||
const txs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(txs);
|
||||
@@ -944,8 +1108,15 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast")));
|
||||
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "chicken_breast"),
|
||||
),
|
||||
);
|
||||
await testDb.db
|
||||
.delete(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||
});
|
||||
|
||||
@@ -966,10 +1137,28 @@ describe("cooking sessions", () => {
|
||||
cookTimeMinutes: 10,
|
||||
totalTimeMinutes: 15,
|
||||
portions: 4,
|
||||
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 100,
|
||||
proteinG: 5,
|
||||
fatG: 3,
|
||||
carbsG: 12,
|
||||
saturatedFatG: 1,
|
||||
fiberG: 1,
|
||||
sugarG: 2,
|
||||
saltG: 0.1,
|
||||
},
|
||||
allergens: [],
|
||||
spiceLevel: 0,
|
||||
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
|
||||
dna: {
|
||||
cuisine: "international",
|
||||
vegetables: [],
|
||||
flavorProfile: [],
|
||||
spiceLevel: 0,
|
||||
method: "stovetop",
|
||||
timeMinutes: 15,
|
||||
calories: 100,
|
||||
proteinGrams: 5,
|
||||
},
|
||||
status: "published",
|
||||
verificationStatus: "unverified",
|
||||
sourceType: "own_editorial",
|
||||
@@ -989,7 +1178,12 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "carrot")));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "carrot"),
|
||||
),
|
||||
);
|
||||
const itemId = await createItemWithPurchase("carrot", "Morötter", 400, "GRAM");
|
||||
|
||||
const start = await app.inject({
|
||||
@@ -1017,7 +1211,11 @@ describe("cooking sessions", () => {
|
||||
expect(item[0]!.quantity).toBeCloseTo(100, 1);
|
||||
|
||||
const txs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(txs);
|
||||
@@ -1025,8 +1223,15 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "carrot")));
|
||||
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "carrot"),
|
||||
),
|
||||
);
|
||||
await testDb.db
|
||||
.delete(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||
});
|
||||
|
||||
@@ -1047,10 +1252,28 @@ describe("cooking sessions", () => {
|
||||
cookTimeMinutes: 10,
|
||||
totalTimeMinutes: 15,
|
||||
portions: 4,
|
||||
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 100,
|
||||
proteinG: 5,
|
||||
fatG: 3,
|
||||
carbsG: 12,
|
||||
saturatedFatG: 1,
|
||||
fiberG: 1,
|
||||
sugarG: 2,
|
||||
saltG: 0.1,
|
||||
},
|
||||
allergens: [],
|
||||
spiceLevel: 0,
|
||||
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
|
||||
dna: {
|
||||
cuisine: "international",
|
||||
vegetables: [],
|
||||
flavorProfile: [],
|
||||
spiceLevel: 0,
|
||||
method: "stovetop",
|
||||
timeMinutes: 15,
|
||||
calories: 100,
|
||||
proteinGrams: 5,
|
||||
},
|
||||
status: "published",
|
||||
verificationStatus: "unverified",
|
||||
sourceType: "own_editorial",
|
||||
@@ -1070,7 +1293,12 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "potato")));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "potato"),
|
||||
),
|
||||
);
|
||||
const itemId = await createItemWithPurchase("potato", "Potatis", 400, "GRAM");
|
||||
|
||||
const start = await app.inject({
|
||||
@@ -1099,13 +1327,20 @@ describe("cooking sessions", () => {
|
||||
expect(item[0]!.quantity).toBeCloseTo(0, 1);
|
||||
|
||||
// Lådan ska ha 1 portion.
|
||||
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
const boxes = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
expect(boxes.length).toBe(1);
|
||||
expect(boxes[0]!.portions).toBe(1);
|
||||
|
||||
// Invariant.
|
||||
const txs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(txs);
|
||||
@@ -1113,8 +1348,15 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "potato")));
|
||||
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "potato"),
|
||||
),
|
||||
);
|
||||
await testDb.db
|
||||
.delete(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||
});
|
||||
|
||||
@@ -1135,10 +1377,28 @@ describe("cooking sessions", () => {
|
||||
cookTimeMinutes: 10,
|
||||
totalTimeMinutes: 15,
|
||||
portions: 4,
|
||||
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 100,
|
||||
proteinG: 5,
|
||||
fatG: 3,
|
||||
carbsG: 12,
|
||||
saturatedFatG: 1,
|
||||
fiberG: 1,
|
||||
sugarG: 2,
|
||||
saltG: 0.1,
|
||||
},
|
||||
allergens: [],
|
||||
spiceLevel: 0,
|
||||
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
|
||||
dna: {
|
||||
cuisine: "international",
|
||||
vegetables: [],
|
||||
flavorProfile: [],
|
||||
spiceLevel: 0,
|
||||
method: "stovetop",
|
||||
timeMinutes: 15,
|
||||
calories: 100,
|
||||
proteinGrams: 5,
|
||||
},
|
||||
status: "published",
|
||||
verificationStatus: "unverified",
|
||||
sourceType: "own_editorial",
|
||||
@@ -1158,7 +1418,12 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "rice_white")));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "rice_white"),
|
||||
),
|
||||
);
|
||||
const itemId = await createItemWithPurchase("rice_white", "Ris", 400, "GRAM");
|
||||
|
||||
async function assertInvariant() {
|
||||
@@ -1168,7 +1433,11 @@ describe("cooking sessions", () => {
|
||||
.where(eq(schema.inventoryItems.id, itemId))
|
||||
.limit(1);
|
||||
const txs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(txs);
|
||||
@@ -1217,8 +1486,15 @@ describe("cooking sessions", () => {
|
||||
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "rice_white")));
|
||||
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
.where(
|
||||
and(
|
||||
eq(schema.inventoryItems.householdId, householdId),
|
||||
eq(schema.inventoryItems.canonicalIngredientId, "rice_white"),
|
||||
),
|
||||
);
|
||||
await testDb.db
|
||||
.delete(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||
});
|
||||
|
||||
@@ -1247,26 +1523,40 @@ describe("cooking sessions", () => {
|
||||
.limit(1);
|
||||
expect(session[0]!.status).toBe("undone");
|
||||
|
||||
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
const boxes = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
expect(boxes.every((b) => b.status === "discarded")).toBe(true);
|
||||
});
|
||||
|
||||
it("undo rolls back cooking assumption profiles", async () => {
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
await testDb.db
|
||||
.delete(schema.cookingAssumptionProfiles)
|
||||
.where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
|
||||
const recipe = (await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptionalIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId && !i.optional)?.canonicalIngredientId;
|
||||
const recipe = (
|
||||
await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
).json() as { ingredients: Array<{ canonicalIngredientId: string; optional?: boolean }> };
|
||||
const firstNonOptionalIngredientId = recipe.ingredients.find(
|
||||
(i) => i.canonicalIngredientId && !i.optional,
|
||||
)?.canonicalIngredientId;
|
||||
expect(firstNonOptionalIngredientId).toBeDefined();
|
||||
|
||||
const cook = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, actualPortionsEaten: 3, leftoverEstimatePortions: 1, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
actualPortionsEaten: 3,
|
||||
leftoverEstimatePortions: 1,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
const { sessionId } = JSON.parse(cook.body) as { sessionId: string };
|
||||
|
||||
@@ -1315,12 +1605,20 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 1,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
expect(cook1.statusCode).toBe(200);
|
||||
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
|
||||
|
||||
const boxes1 = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId1));
|
||||
const boxes1 = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId1));
|
||||
expect(boxes1.length).toBe(1);
|
||||
const boxId = boxes1[0]!.id;
|
||||
|
||||
@@ -1328,12 +1626,20 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 2, mealBoxFrozen: false, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 2,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
expect(cook2.statusCode).toBe(200);
|
||||
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
|
||||
|
||||
const boxes2 = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.id, boxId));
|
||||
const boxes2 = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.id, boxId));
|
||||
expect(boxes2[0]!.portions).toBe(3);
|
||||
expect(boxes2[0]!.portionsRemaining).toBe(3);
|
||||
|
||||
@@ -1352,7 +1658,13 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, date: yesterday, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 1,
|
||||
mealBoxFrozen: false,
|
||||
date: yesterday,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
expect(cook1.statusCode).toBe(200);
|
||||
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
|
||||
@@ -1361,7 +1673,12 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 1,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
expect(cook2.statusCode).toBe(200);
|
||||
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
|
||||
@@ -1385,7 +1702,12 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 1,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
expect(cook1.statusCode).toBe(200);
|
||||
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
|
||||
@@ -1394,7 +1716,12 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: true, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 1,
|
||||
mealBoxFrozen: true,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
expect(cook2.statusCode).toBe(200);
|
||||
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
|
||||
@@ -1420,20 +1747,38 @@ describe("cooking sessions", () => {
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 1, mealBoxFrozen: false, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 1,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
const { sessionId: sessionId1 } = JSON.parse(cook1.body) as { sessionId: string };
|
||||
const boxId = (await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId1)))[0]!.id;
|
||||
const boxId = (
|
||||
await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId1))
|
||||
)[0]!.id;
|
||||
|
||||
const cook2 = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { portionsCooked: 4, mealBoxPortions: 2, mealBoxFrozen: false, deductInventory: true },
|
||||
payload: {
|
||||
portionsCooked: 4,
|
||||
mealBoxPortions: 2,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
},
|
||||
});
|
||||
const { sessionId: sessionId2 } = JSON.parse(cook2.body) as { sessionId: string };
|
||||
|
||||
const beforeUndo = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.id, boxId));
|
||||
const beforeUndo = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.id, boxId));
|
||||
expect(beforeUndo[0]!.portions).toBe(3);
|
||||
|
||||
const undo = await app.inject({
|
||||
@@ -1444,7 +1789,10 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
expect(undo.statusCode).toBe(200);
|
||||
|
||||
const afterUndo = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.id, boxId));
|
||||
const afterUndo = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.id, boxId));
|
||||
expect(afterUndo[0]!.portions).toBe(1);
|
||||
expect(afterUndo[0]!.portionsRemaining).toBe(1);
|
||||
expect(afterUndo[0]!.status).toBe("available");
|
||||
@@ -1469,7 +1817,10 @@ describe("cooking sessions", () => {
|
||||
});
|
||||
expect(undo.statusCode).toBe(200);
|
||||
|
||||
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
const boxes = await testDb.db
|
||||
.select()
|
||||
.from(schema.mealBoxes)
|
||||
.where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||
expect(boxes[0]!.status).toBe("discarded");
|
||||
expect(boxes[0]!.portionsRemaining).toBe(0);
|
||||
});
|
||||
@@ -1504,7 +1855,11 @@ describe("cooking sessions", () => {
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(txs);
|
||||
const item = await testDb.db.select().from(schema.inventoryItems).where(eq(schema.inventoryItems.id, itemId)).limit(1);
|
||||
const item = await testDb.db
|
||||
.select()
|
||||
.from(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.id, itemId))
|
||||
.limit(1);
|
||||
expect(item[0]!.quantity).toBeCloseTo(balance.balance, 6);
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ describe("S6 minnes-i18n + transparens-paritet", () => {
|
||||
expect(renderMemorySummary({ summarySv: "", value }, "sv-SE")).toBe(expectedSv);
|
||||
for (const lang of EXPECTED_LANGS) {
|
||||
const rendered = renderMemorySummary({ summarySv: "", value }, `${lang}-XX`);
|
||||
expect(rendered.length, `tom summary för ${lang}, ${JSON.stringify(value)}`).toBeGreaterThan(0);
|
||||
expect(
|
||||
rendered.length,
|
||||
`tom summary för ${lang}, ${JSON.stringify(value)}`,
|
||||
).toBeGreaterThan(0);
|
||||
expect(rendered).not.toBe("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +52,18 @@ describe("GET /v1/inventory/natural-search", () => {
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
const householdIds = memberships.map((m) => m.householdId);
|
||||
if (householdIds.length > 0) {
|
||||
await testDb.db.delete(schema.inventoryItems).where(inArray(schema.inventoryItems.householdId, householdIds));
|
||||
await testDb.db.delete(schema.storageLocations).where(inArray(schema.storageLocations.householdId, householdIds));
|
||||
await testDb.db.delete(schema.householdMembers).where(inArray(schema.householdMembers.householdId, householdIds));
|
||||
await testDb.db.delete(schema.households).where(inArray(schema.households.id, householdIds));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(inArray(schema.inventoryItems.householdId, householdIds));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(inArray(schema.storageLocations.householdId, householdIds));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(inArray(schema.householdMembers.householdId, householdIds));
|
||||
await testDb.db
|
||||
.delete(schema.households)
|
||||
.where(inArray(schema.households.id, householdIds));
|
||||
}
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
}
|
||||
@@ -69,7 +77,10 @@ describe("GET /v1/inventory/natural-search", () => {
|
||||
const [household] = await testDb.db
|
||||
.select({ id: schema.households.id })
|
||||
.from(schema.households)
|
||||
.innerJoin(schema.householdMembers, eq(schema.households.id, schema.householdMembers.householdId))
|
||||
.innerJoin(
|
||||
schema.householdMembers,
|
||||
eq(schema.households.id, schema.householdMembers.householdId),
|
||||
)
|
||||
.where(eq(schema.householdMembers.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
|
||||
@@ -20,18 +20,29 @@ describe("inventory trust read-time computation", () => {
|
||||
.from(schema.users)
|
||||
.where(inArray(schema.users.email, [userEmail]));
|
||||
for (const u of existing) {
|
||||
await testDb.db.delete(schema.inventoryTransactions).where(eq(schema.inventoryTransactions.actorUserId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.actorUserId, u.id));
|
||||
const owned = await testDb.db
|
||||
.select({ id: schema.households.id })
|
||||
.from(schema.households)
|
||||
.innerJoin(schema.householdMembers, eq(schema.householdMembers.householdId, schema.households.id))
|
||||
.innerJoin(
|
||||
schema.householdMembers,
|
||||
eq(schema.householdMembers.householdId, schema.households.id),
|
||||
)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
for (const h of owned) {
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, h.id));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, h.id));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, h.id));
|
||||
}
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
}
|
||||
@@ -99,7 +110,9 @@ describe("inventory trust read-time computation", () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
const body = JSON.parse(res.body) as { items: Array<{ id: string; trustState: string; trustScore: number }> };
|
||||
const body = JSON.parse(res.body) as {
|
||||
items: Array<{ id: string; trustState: string; trustScore: number }>;
|
||||
};
|
||||
const found = body.items.find((i) => i.id === item!.id);
|
||||
expect(found).toBeTruthy();
|
||||
expect(["decaying", "stale"]).toContain(found!.trustState);
|
||||
|
||||
@@ -28,8 +28,12 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
await testDb.db.delete(schema.recipeRatings).where(eq(schema.recipeRatings.userId, u.id));
|
||||
await testDb.db.delete(schema.recipeFavorites).where(eq(schema.recipeFavorites.userId, u.id));
|
||||
await testDb.db.delete(schema.recipeCooks).where(eq(schema.recipeCooks.userId, u.id));
|
||||
await testDb.db.delete(schema.creatorFollows).where(eq(schema.creatorFollows.followerUserId, u.id));
|
||||
await testDb.db.delete(schema.creatorFollows).where(eq(schema.creatorFollows.creatorUserId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.creatorFollows)
|
||||
.where(eq(schema.creatorFollows.followerUserId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.creatorFollows)
|
||||
.where(eq(schema.creatorFollows.creatorUserId, u.id));
|
||||
await testDb.db.delete(schema.creatorStats).where(eq(schema.creatorStats.userId, u.id));
|
||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.creatorUserId, u.id));
|
||||
await testDb.db.delete(schema.foodMemories).where(eq(schema.foodMemories.userId, u.id));
|
||||
@@ -37,8 +41,12 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
await testDb.db.delete(schema.tasteSignals).where(eq(schema.tasteSignals.userId, u.id));
|
||||
await testDb.db.delete(schema.meals).where(eq(schema.meals.userId, u.id));
|
||||
await testDb.db.delete(schema.subscriptions).where(eq(schema.subscriptions.userId, u.id));
|
||||
await testDb.db.delete(schema.subscriptionEvents).where(eq(schema.subscriptionEvents.userId, u.id));
|
||||
await testDb.db.delete(schema.storeTransactions).where(eq(schema.storeTransactions.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.subscriptionEvents)
|
||||
.where(eq(schema.subscriptionEvents.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.storeTransactions)
|
||||
.where(eq(schema.storeTransactions.userId, u.id));
|
||||
await testDb.db.delete(schema.trials).where(eq(schema.trials.userId, u.id));
|
||||
await testDb.db.delete(schema.aiUsageCounters).where(eq(schema.aiUsageCounters.userId, u.id));
|
||||
await testDb.db.delete(schema.notifications).where(eq(schema.notifications.userId, u.id));
|
||||
@@ -46,22 +54,34 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
await testDb.db.delete(schema.idempotencyKeys).where(eq(schema.idempotencyKeys.userId, u.id));
|
||||
await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, u.id));
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.userHealthProfiles).where(eq(schema.userHealthProfiles.userId, u.id));
|
||||
await testDb.db.delete(schema.userLocalePreferences).where(eq(schema.userLocalePreferences.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.userHealthProfiles)
|
||||
.where(eq(schema.userHealthProfiles.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.userLocalePreferences)
|
||||
.where(eq(schema.userLocalePreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.userCredentials).where(eq(schema.userCredentials.userId, u.id));
|
||||
await testDb.db.delete(schema.refreshTokens).where(eq(schema.refreshTokens.userId, u.id));
|
||||
await testDb.db.delete(schema.emailVerificationTokens).where(eq(schema.emailVerificationTokens.userId, u.id));
|
||||
await testDb.db.delete(schema.passwordResetTokens).where(eq(schema.passwordResetTokens.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.emailVerificationTokens)
|
||||
.where(eq(schema.emailVerificationTokens.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.passwordResetTokens)
|
||||
.where(eq(schema.passwordResetTokens.userId, u.id));
|
||||
await testDb.db.delete(schema.adminTotp).where(eq(schema.adminTotp.userId, u.id));
|
||||
await testDb.db.delete(schema.auditLogs).where(eq(schema.auditLogs.actorUserId, u.id));
|
||||
await testDb.db.delete(schema.domainEvents).where(eq(schema.domainEvents.userId, u.id));
|
||||
await testDb.db.delete(schema.productAnalyticsEvents).where(eq(schema.productAnalyticsEvents.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.productAnalyticsEvents)
|
||||
.where(eq(schema.productAnalyticsEvents.userId, u.id));
|
||||
// Remove memberships and any orphaned single-member households.
|
||||
const memberships = await testDb.db
|
||||
.select({ householdId: schema.householdMembers.householdId })
|
||||
.from(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
for (const { householdId } of memberships) {
|
||||
const remaining = await testDb.db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
@@ -124,7 +144,9 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
{ userId, kind: "personalization", status: "granted" },
|
||||
{ userId, kind: "image_training", status: "granted" },
|
||||
]);
|
||||
await testDb.db.insert(schema.pushTokens).values({ userId, token: "expo-token-1", platform: "ios" });
|
||||
await testDb.db
|
||||
.insert(schema.pushTokens)
|
||||
.values({ userId, token: "expo-token-1", platform: "ios" });
|
||||
await testDb.db.insert(schema.notifications).values({
|
||||
userId,
|
||||
type: "subscription_status",
|
||||
@@ -201,7 +223,16 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
source: "manual",
|
||||
titleSv: "Resttest-lunch",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
nutrition: { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0 },
|
||||
nutrition: {
|
||||
kcal: 0,
|
||||
proteinG: 0,
|
||||
carbsG: 0,
|
||||
fatG: 0,
|
||||
saturatedFatG: 0,
|
||||
fiberG: 0,
|
||||
sugarG: 0,
|
||||
saltG: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [draftRecipe] = await testDb.db
|
||||
@@ -215,7 +246,16 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
creatorUserId: userId,
|
||||
creatorDisplayName: "Test",
|
||||
status: "draft",
|
||||
nutritionPerPortion: { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 0,
|
||||
proteinG: 0,
|
||||
carbsG: 0,
|
||||
fatG: 0,
|
||||
saturatedFatG: 0,
|
||||
fiberG: 0,
|
||||
sugarG: 0,
|
||||
saltG: 0,
|
||||
},
|
||||
dna: {
|
||||
cuisine: "swedish",
|
||||
vegetables: [],
|
||||
@@ -241,7 +281,16 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
creatorDisplayName: "Test",
|
||||
status: "published",
|
||||
verificationStatus: "editorial",
|
||||
nutritionPerPortion: { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0 },
|
||||
nutritionPerPortion: {
|
||||
kcal: 0,
|
||||
proteinG: 0,
|
||||
carbsG: 0,
|
||||
fatG: 0,
|
||||
saturatedFatG: 0,
|
||||
fiberG: 0,
|
||||
sugarG: 0,
|
||||
saltG: 0,
|
||||
},
|
||||
dna: {
|
||||
cuisine: "swedish",
|
||||
vegetables: [],
|
||||
@@ -255,9 +304,15 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
})
|
||||
.returning();
|
||||
|
||||
await testDb.db.insert(schema.recipeRatings).values({ recipeId: publishedRecipe!.id, userId, stars: 5 });
|
||||
await testDb.db.insert(schema.recipeFavorites).values({ recipeId: publishedRecipe!.id, userId });
|
||||
await testDb.db.insert(schema.recipeCooks).values({ recipeId: publishedRecipe!.id, userId, portionsCooked: 2 });
|
||||
await testDb.db
|
||||
.insert(schema.recipeRatings)
|
||||
.values({ recipeId: publishedRecipe!.id, userId, stars: 5 });
|
||||
await testDb.db
|
||||
.insert(schema.recipeFavorites)
|
||||
.values({ recipeId: publishedRecipe!.id, userId });
|
||||
await testDb.db
|
||||
.insert(schema.recipeCooks)
|
||||
.values({ recipeId: publishedRecipe!.id, userId, portionsCooked: 2 });
|
||||
await testDb.db.insert(schema.creatorStats).values({ userId });
|
||||
|
||||
await testDb.db.insert(schema.subscriptions).values({
|
||||
@@ -431,7 +486,10 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
const result = await testDb.db.execute<{ count: number }>(
|
||||
sql.raw(`SELECT count(*)::int AS count FROM "${table}" WHERE "${column}" = '${userId}'`),
|
||||
);
|
||||
expect(Number(result.rows[0]?.count ?? 0), `Residual ${table}.${column} for deleted user`).toBe(0);
|
||||
expect(
|
||||
Number(result.rows[0]?.count ?? 0),
|
||||
`Residual ${table}.${column} for deleted user`,
|
||||
).toBe(0);
|
||||
}
|
||||
|
||||
// Receipts in surviving households must have image stripped.
|
||||
@@ -440,12 +498,18 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
.from(schema.receipts)
|
||||
.where(eq(schema.receipts.householdId, (await getHouseholdId(userId))!));
|
||||
for (const r of survivingReceipts) {
|
||||
expect(r.imageUrl, "Receipt image must be null after deletion in surviving household").toBeNull();
|
||||
expect(
|
||||
r.imageUrl,
|
||||
"Receipt image must be null after deletion in surviving household",
|
||||
).toBeNull();
|
||||
}
|
||||
|
||||
// Public recipe must be anonymized.
|
||||
const [publicAfter] = await testDb.db
|
||||
.select({ creatorUserId: schema.recipes.creatorUserId, creatorDisplayName: schema.recipes.creatorDisplayName })
|
||||
.select({
|
||||
creatorUserId: schema.recipes.creatorUserId,
|
||||
creatorDisplayName: schema.recipes.creatorDisplayName,
|
||||
})
|
||||
.from(schema.recipes)
|
||||
.where(eq(schema.recipes.id, publishedRecipe!.id));
|
||||
expect(publicAfter?.creatorUserId).toBeNull();
|
||||
@@ -460,7 +524,11 @@ describe("DELETE /v1/me — GDPR residual completeness", () => {
|
||||
|
||||
// User row is soft-deleted and anonymized.
|
||||
const [userAfter] = await testDb.db
|
||||
.select({ email: schema.users.email, displayName: schema.users.displayName, deletedAt: schema.users.deletedAt })
|
||||
.select({
|
||||
email: schema.users.email,
|
||||
displayName: schema.users.displayName,
|
||||
deletedAt: schema.users.deletedAt,
|
||||
})
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.id, userId));
|
||||
expect(userAfter?.deletedAt).not.toBeNull();
|
||||
|
||||
@@ -21,14 +21,12 @@ describe("DELETE /v1/me — GDPR-radering", () => {
|
||||
.from(schema.aiCorrections)
|
||||
.where(eq(schema.aiCorrections.userId, u.id));
|
||||
if (corrections.length > 0) {
|
||||
await testDb.db
|
||||
.delete(schema.aiTrainingBank)
|
||||
.where(
|
||||
inArray(
|
||||
schema.aiTrainingBank.correctionId,
|
||||
corrections.map((r) => r.id),
|
||||
),
|
||||
);
|
||||
await testDb.db.delete(schema.aiTrainingBank).where(
|
||||
inArray(
|
||||
schema.aiTrainingBank.correctionId,
|
||||
corrections.map((r) => r.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
await testDb.db.delete(schema.aiCorrections).where(eq(schema.aiCorrections.userId, u.id));
|
||||
await testDb.db.delete(schema.scanJobs).where(eq(schema.scanJobs.userId, u.id));
|
||||
|
||||
@@ -41,7 +41,9 @@ describe("/v1/me/memory", () => {
|
||||
await testDb.db.delete(schema.tasteSignals).where(eq(schema.tasteSignals.userId, u.id));
|
||||
await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, u.id));
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
const ownedHouseholds = await testDb.db
|
||||
.select({ id: schema.households.id })
|
||||
.from(schema.households)
|
||||
@@ -53,7 +55,9 @@ describe("/v1/me/memory", () => {
|
||||
and(eq(schema.householdMembers.userId, u.id), eq(schema.householdMembers.role, "owner")),
|
||||
);
|
||||
for (const h of ownedHouseholds) {
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, h.id));
|
||||
}
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
@@ -100,7 +104,11 @@ describe("/v1/me/memory", () => {
|
||||
});
|
||||
|
||||
expect(patchRes.statusCode).toBe(200);
|
||||
const body = JSON.parse(patchRes.body) as { origin: string; confidence: number; verifiedByUser: boolean };
|
||||
const body = JSON.parse(patchRes.body) as {
|
||||
origin: string;
|
||||
confidence: number;
|
||||
verifiedByUser: boolean;
|
||||
};
|
||||
expect(body.origin).toBe("user_stated");
|
||||
expect(body.confidence).toBe(1);
|
||||
expect(body.verifiedByUser).toBe(true);
|
||||
@@ -237,7 +245,9 @@ describe("DELETE /v1/me/memory GDPR-regression", () => {
|
||||
await testDb.db.delete(schema.tasteSignals).where(eq(schema.tasteSignals.userId, u.id));
|
||||
await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, u.id));
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.userId, u.id));
|
||||
const ownedHouseholds = await testDb.db
|
||||
.select({ id: schema.households.id })
|
||||
.from(schema.households)
|
||||
@@ -249,7 +259,9 @@ describe("DELETE /v1/me/memory GDPR-regression", () => {
|
||||
and(eq(schema.householdMembers.userId, u.id), eq(schema.householdMembers.role, "owner")),
|
||||
);
|
||||
for (const h of ownedHouseholds) {
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, h.id));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, h.id));
|
||||
}
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
|
||||
@@ -19,7 +19,9 @@ describe("S5 — onboarding → minne + smaksignaler", () => {
|
||||
await testDb.db.delete(schema.memoryItems).where(eq(schema.memoryItems.userId, u.id));
|
||||
await testDb.db.delete(schema.tasteSignals).where(eq(schema.tasteSignals.userId, u.id));
|
||||
await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
|
||||
await testDb.db.delete(schema.userHealthProfiles).where(eq(schema.userHealthProfiles.userId, u.id));
|
||||
await testDb.db
|
||||
.delete(schema.userHealthProfiles)
|
||||
.where(eq(schema.userHealthProfiles.userId, u.id));
|
||||
await testDb.db.delete(schema.userCredentials).where(eq(schema.userCredentials.userId, u.id));
|
||||
await testDb.db.delete(schema.refreshTokens).where(eq(schema.refreshTokens.userId, u.id));
|
||||
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
||||
@@ -102,16 +104,36 @@ describe("S5 — onboarding → minne + smaksignaler", () => {
|
||||
.select()
|
||||
.from(schema.tasteSignals)
|
||||
.where(eq(schema.tasteSignals.userId, userId));
|
||||
const cuisineSignals = signals.filter((s) => s.axis === "cuisine").sort((a, b) => (a.target ?? "").localeCompare(b.target ?? ""));
|
||||
const avoidSignals = signals.filter((s) => s.axis === "ingredient_avoid").sort((a, b) => (a.target ?? "").localeCompare(b.target ?? ""));
|
||||
const cuisineSignals = signals
|
||||
.filter((s) => s.axis === "cuisine")
|
||||
.sort((a, b) => (a.target ?? "").localeCompare(b.target ?? ""));
|
||||
const avoidSignals = signals
|
||||
.filter((s) => s.axis === "ingredient_avoid")
|
||||
.sort((a, b) => (a.target ?? "").localeCompare(b.target ?? ""));
|
||||
|
||||
expect(cuisineSignals).toHaveLength(2);
|
||||
expect(cuisineSignals[0]).toMatchObject({ target: "italian", direction: 1, origin: "user_stated" });
|
||||
expect(cuisineSignals[1]).toMatchObject({ target: "thai", direction: 1, origin: "user_stated" });
|
||||
expect(cuisineSignals[0]).toMatchObject({
|
||||
target: "italian",
|
||||
direction: 1,
|
||||
origin: "user_stated",
|
||||
});
|
||||
expect(cuisineSignals[1]).toMatchObject({
|
||||
target: "thai",
|
||||
direction: 1,
|
||||
origin: "user_stated",
|
||||
});
|
||||
|
||||
expect(avoidSignals).toHaveLength(2);
|
||||
expect(avoidSignals[0]).toMatchObject({ target: "anchovy", direction: -1, origin: "user_stated" });
|
||||
expect(avoidSignals[1]).toMatchObject({ target: "broccoli", direction: -1, origin: "user_stated" });
|
||||
expect(avoidSignals[0]).toMatchObject({
|
||||
target: "anchovy",
|
||||
direction: -1,
|
||||
origin: "user_stated",
|
||||
});
|
||||
expect(avoidSignals[1]).toMatchObject({
|
||||
target: "broccoli",
|
||||
direction: -1,
|
||||
origin: "user_stated",
|
||||
});
|
||||
|
||||
await cleanupUser(email);
|
||||
});
|
||||
@@ -219,8 +241,14 @@ describe("S5 — onboarding → minne + smaksignaler", () => {
|
||||
.from(schema.tasteSignals)
|
||||
.where(eq(schema.tasteSignals.userId, userId));
|
||||
expect(signals).toHaveLength(2);
|
||||
expect(signals.some((s) => s.axis === "cuisine" && s.target === "italian" && s.direction === 1)).toBe(true);
|
||||
expect(signals.some((s) => s.axis === "ingredient_avoid" && s.target === "mushroom" && s.direction === -1)).toBe(true);
|
||||
expect(
|
||||
signals.some((s) => s.axis === "cuisine" && s.target === "italian" && s.direction === 1),
|
||||
).toBe(true);
|
||||
expect(
|
||||
signals.some(
|
||||
(s) => s.axis === "ingredient_avoid" && s.target === "mushroom" && s.direction === -1,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
await cleanupUser(email);
|
||||
});
|
||||
|
||||
@@ -29,15 +29,13 @@ describe("progressive onboarding validering (FAS 1b)", () => {
|
||||
});
|
||||
|
||||
it("quick-start avvisar ogiltigt mål", () => {
|
||||
expect(() =>
|
||||
parse(quickStartInputSchema, { primaryGoal: "invalid_goal" }),
|
||||
).toThrowError(ApiError);
|
||||
expect(() => parse(quickStartInputSchema, { primaryGoal: "invalid_goal" })).toThrowError(
|
||||
ApiError,
|
||||
);
|
||||
});
|
||||
|
||||
it("quick-start avvisar ogiltig precision", () => {
|
||||
expect(() =>
|
||||
parse(quickStartInputSchema, { precisionMode: "medium" }),
|
||||
).toThrowError(ApiError);
|
||||
expect(() => parse(quickStartInputSchema, { precisionMode: "medium" })).toThrowError(ApiError);
|
||||
});
|
||||
|
||||
it("onboarding-status schema validerar korrekt struktur", () => {
|
||||
|
||||
@@ -30,11 +30,19 @@ describe("quick reconciliation", () => {
|
||||
.from(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
for (const it of items) {
|
||||
await testDb.db.delete(schema.inventoryTransactions).where(eq(schema.inventoryTransactions.inventoryItemId, it.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, it.id));
|
||||
}
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
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));
|
||||
@@ -97,7 +105,9 @@ describe("quick reconciliation", () => {
|
||||
payload: {},
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { candidates: Array<{ itemId: string; reasons: unknown[]; suggestedAction: string }> };
|
||||
const body = JSON.parse(res.body) as {
|
||||
candidates: Array<{ itemId: string; reasons: unknown[]; suggestedAction: string }>;
|
||||
};
|
||||
expect(body.candidates.length).toBeGreaterThan(0);
|
||||
expect(body.candidates[0]?.reasons.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -119,7 +129,11 @@ describe("quick reconciliation", () => {
|
||||
payload: { action: "exists", quantity: 0.5 },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { action: string; quantity: number; verifiedByUser: boolean };
|
||||
const body = JSON.parse(res.body) as {
|
||||
action: string;
|
||||
quantity: number;
|
||||
verifiedByUser: boolean;
|
||||
};
|
||||
expect(body.action).toBe("exists");
|
||||
expect(body.quantity).toBe(0.5);
|
||||
expect(body.verifiedByUser).toBe(true);
|
||||
@@ -142,10 +156,16 @@ describe("quick reconciliation", () => {
|
||||
.where(eq(schema.inventoryItems.id, itemId))
|
||||
.limit(1);
|
||||
const txs = await testDb.db
|
||||
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||
.select({
|
||||
type: schema.inventoryTransactions.type,
|
||||
quantityDelta: schema.inventoryTransactions.quantityDelta,
|
||||
unit: schema.inventoryTransactions.unit,
|
||||
})
|
||||
.from(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||
const balance = computeBalance(txs.map((tx) => ({ type: tx.type, quantityDelta: tx.quantityDelta, unit: tx.unit })));
|
||||
const balance = computeBalance(
|
||||
txs.map((tx) => ({ type: tx.type, quantityDelta: tx.quantityDelta, unit: tx.unit })),
|
||||
);
|
||||
expect(balance.balance).toBeCloseTo(item!.quantity, 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,13 +31,25 @@ describe("scan-to-scan-diff", () => {
|
||||
.from(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
for (const it of items) {
|
||||
await testDb.db.delete(schema.inventoryTransactions).where(eq(schema.inventoryTransactions.inventoryItemId, it.id));
|
||||
await testDb.db.delete(schema.inventoryConflicts).where(eq(schema.inventoryConflicts.inventoryItemId, it.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, it.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryConflicts)
|
||||
.where(eq(schema.inventoryConflicts.inventoryItemId, it.id));
|
||||
}
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.inventoryConflicts).where(eq(schema.inventoryConflicts.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryConflicts)
|
||||
.where(eq(schema.inventoryConflicts.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, m.householdId));
|
||||
}
|
||||
await testDb.db.delete(schema.scanJobs).where(eq(schema.scanJobs.userId, u.id));
|
||||
@@ -107,8 +119,20 @@ describe("scan-to-scan-diff", () => {
|
||||
jobType: "ANALYZE_FRIDGE_IMAGE",
|
||||
status: "completed",
|
||||
result: [
|
||||
{ displayName: "Mjölk", quantity: 0.5, unit: "LITER", storageLocationId: location!.id, confidence: 0.9 },
|
||||
{ displayName: "Ost", quantity: 1, unit: "COUNT", storageLocationId: location!.id, confidence: 0.9 },
|
||||
{
|
||||
displayName: "Mjölk",
|
||||
quantity: 0.5,
|
||||
unit: "LITER",
|
||||
storageLocationId: location!.id,
|
||||
confidence: 0.9,
|
||||
},
|
||||
{
|
||||
displayName: "Ost",
|
||||
quantity: 1,
|
||||
unit: "COUNT",
|
||||
storageLocationId: location!.id,
|
||||
confidence: 0.9,
|
||||
},
|
||||
],
|
||||
modelVersion: "v1",
|
||||
promptVersion: "p1",
|
||||
@@ -151,7 +175,17 @@ describe("scan-to-scan-diff", () => {
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {},
|
||||
});
|
||||
const { rows } = JSON.parse(diff.body) as { rows: Array<{ kind: string; itemId?: string; previousItemId?: string; displayName: string; newQuantity?: number; newLocationId?: string; confidence: number }> };
|
||||
const { rows } = JSON.parse(diff.body) as {
|
||||
rows: Array<{
|
||||
kind: string;
|
||||
itemId?: string;
|
||||
previousItemId?: string;
|
||||
displayName: string;
|
||||
newQuantity?: number;
|
||||
newLocationId?: string;
|
||||
confidence: number;
|
||||
}>;
|
||||
};
|
||||
const changed = rows.find((r) => r.kind === "quantity_changed")!;
|
||||
|
||||
const apply = await app.inject({
|
||||
|
||||
@@ -33,13 +33,25 @@ describe("scan confirmation → ai_corrections", () => {
|
||||
.from(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
for (const it of items) {
|
||||
await testDb.db.delete(schema.inventoryTransactions).where(eq(schema.inventoryTransactions.inventoryItemId, it.id));
|
||||
await testDb.db.delete(schema.inventoryConflicts).where(eq(schema.inventoryConflicts.inventoryItemId, it.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryTransactions)
|
||||
.where(eq(schema.inventoryTransactions.inventoryItemId, it.id));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryConflicts)
|
||||
.where(eq(schema.inventoryConflicts.inventoryItemId, it.id));
|
||||
}
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.inventoryConflicts).where(eq(schema.inventoryConflicts.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryItems)
|
||||
.where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.inventoryConflicts)
|
||||
.where(eq(schema.inventoryConflicts.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.storageLocations)
|
||||
.where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db
|
||||
.delete(schema.householdMembers)
|
||||
.where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, m.householdId));
|
||||
}
|
||||
await testDb.db.delete(schema.scanJobs).where(eq(schema.scanJobs.userId, u.id));
|
||||
@@ -62,7 +74,11 @@ describe("scan confirmation → ai_corrections", () => {
|
||||
}
|
||||
await testDb.db
|
||||
.insert(schema.userConsents)
|
||||
.values({ userId, kind: "image_training" as const, status: (imageTraining ? "granted" : "denied") as "granted" | "denied" })
|
||||
.values({
|
||||
userId,
|
||||
kind: "image_training" as const,
|
||||
status: (imageTraining ? "granted" : "denied") as "granted" | "denied",
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.userConsents.userId, schema.userConsents.kind],
|
||||
set: { status: (imageTraining ? "granted" : "denied") as "granted" | "denied" },
|
||||
@@ -168,7 +184,9 @@ describe("scan confirmation → ai_corrections", () => {
|
||||
expect(corrections).toHaveLength(1);
|
||||
expect((corrections[0]!.userCorrection as Record<string, string>).action).toBe("accept");
|
||||
expect((corrections[0]!.proposal as Record<string, unknown>).detectedName).toBe("Mellanmjölk");
|
||||
expect((corrections[0]!.userCorrection as Record<string, Record<string, unknown>>).corrected).toMatchObject({
|
||||
expect(
|
||||
(corrections[0]!.userCorrection as Record<string, Record<string, unknown>>).corrected,
|
||||
).toMatchObject({
|
||||
displayName: "Mellanmjölk",
|
||||
quantity: 1,
|
||||
unit: "LITER",
|
||||
@@ -203,7 +221,9 @@ describe("scan confirmation → ai_corrections", () => {
|
||||
.from(schema.aiCorrections)
|
||||
.where(eq(schema.aiCorrections.scanJobId, scanJobId));
|
||||
expect(corrections[0]!.imageS3Key).toBe("fridge-scans/test-image.jpg");
|
||||
expect((corrections[0]!.consentSnapshot as Record<string, string>).image_training).toBe("granted");
|
||||
expect((corrections[0]!.consentSnapshot as Record<string, string>).image_training).toBe(
|
||||
"granted",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not save image reference when image_training consent is denied", async () => {
|
||||
|
||||
Reference in New Issue
Block a user