import { describe, expect, it } from "vitest"; import { PLAN_DEFINITIONS } from "@app/shared-types"; import { canUseAiScan, computeEntitlements, resolvePlanFromProductId, signEntitlementToken, trialEndsAt, verifyEntitlementToken, } from "../src/index.js"; const NOW = new Date("2026-08-02T12:00:00Z"); const SECRET = "test-secret-with-length"; describe("entitlements (spec §45–47)", () => { it("aktiv prenumeration ger planens förmåner", () => { const ent = computeEntitlements( { plan: "family", status: "active", expiresAt: new Date("2026-09-01") }, null, { aiScansUsedThisMonth: 10 }, NOW, ); expect(ent.plan).toBe("family"); expect(ent.maxHouseholdMembers).toBe(6); expect(ent.weekPlanning).toBe(true); }); it("grace period behåller åtkomst (spec §44)", () => { const ent = computeEntitlements( { plan: "household", status: "in_grace", expiresAt: new Date("2026-08-01"), gracePeriodExpiresAt: new Date("2026-08-10"), }, null, { aiScansUsedThisMonth: 0 }, NOW, ); expect(ent.status).toBe("in_grace"); expect(ent.weekPlanning).toBe(true); }); it("utgången prenumeration utan grace → free", () => { const ent = computeEntitlements( { plan: "family", status: "active", expiresAt: new Date("2026-07-01") }, null, { aiScansUsedThisMonth: 0 }, NOW, ); expect(ent.plan).toBe("free"); }); it("trial ger full tillgång i 7 dagar (spec §46)", () => { const start = new Date("2026-08-01T00:00:00Z"); const ent = computeEntitlements( null, { startedAt: start, endsAt: trialEndsAt(start) }, { aiScansUsedThisMonth: 3 }, NOW, ); expect(ent.status).toBe("trial"); expect(ent.weekPlanning).toBe(true); }); it("free tier: 10 skanningar och användbar grund (spec §46)", () => { const ent = computeEntitlements(null, null, { aiScansUsedThisMonth: 9 }, NOW); expect(ent.plan).toBe("free"); expect(ent.aiScansPerMonth).toBe(10); expect(canUseAiScan(ent)).toBe(true); expect(canUseAiScan({ ...ent, aiScansUsedThisMonth: 10 })).toBe(false); }); it("mappar productId → plan (härlett ur brand-konfig, aldrig hårdkodat)", () => { expect(resolvePlanFromProductId(PLAN_DEFINITIONS.family.appleProductId)).toBe("family"); expect(resolvePlanFromProductId(PLAN_DEFINITIONS.household.googleProductId)).toBe("household"); expect(resolvePlanFromProductId("okänd")).toBeNull(); }); }); describe("signerad entitlement-token (spec §44)", () => { const ent = computeEntitlements( { plan: "family", status: "active", expiresAt: new Date("2026-09-01") }, null, { aiScansUsedThisMonth: 0 }, NOW, ); it("signerar och verifierar", () => { const token = signEntitlementToken("user-1", ent, SECRET, { now: NOW }); const result = verifyEntitlementToken(token, SECRET, NOW); expect(result.valid).toBe(true); if (result.valid) { expect(result.payload.sub).toBe("user-1"); expect(result.payload.plan).toBe("family"); expect(result.withinGrace).toBe(false); } }); it("avvisar manipulerad token", () => { const token = signEntitlementToken("user-1", ent, SECRET, { now: NOW }); const tampered = token.slice(0, -4) + "AAAA"; expect(verifyEntitlementToken(tampered, SECRET, NOW).valid).toBe(false); }); it("avvisar fel secret", () => { const token = signEntitlementToken("user-1", ent, SECRET, { now: NOW }); expect(verifyEntitlementToken(token, "annan-secret-123", NOW).valid).toBe(false); }); it("grace: giltig efter exp men inom graceExp, ogiltig därefter – ALDRIG permanent boolean", () => { const token = signEntitlementToken("user-1", ent, SECRET, { now: NOW, ttlHours: 24, graceDays: 7, }); const afterExp = new Date(NOW.getTime() + 3 * 86_400_000); const inGrace = verifyEntitlementToken(token, SECRET, afterExp); expect(inGrace.valid).toBe(true); if (inGrace.valid) expect(inGrace.withinGrace).toBe(true); const afterGrace = new Date(NOW.getTime() + 9 * 86_400_000); const expired = verifyEntitlementToken(token, SECRET, afterGrace); expect(expired.valid).toBe(false); if (!expired.valid) expect(expired.reason).toBe("expired_beyond_grace"); }); });