Files
Cibello-app/packages/subscriptions/test/subscriptions.test.ts
T
Claude 75591bb294 fix(subscriptions): free-niva 5 AI-skanningar (beslut Johan)
Free 10->5 i kod, test, paywall-copy (12 sprak) och docs. Full trappa: free 5 / trial 14d 25 skann / premium 100/200/400.
2026-08-17 17:34:55 +00:00

120 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { 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 §4547)", () => {
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 14 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: 5 skanningar och användbar grund (spec §46)", () => {
const ent = computeEntitlements(null, null, { aiScansUsedThisMonth: 4 }, NOW);
expect(ent.plan).toBe("free");
expect(ent.aiScansPerMonth).toBe(5);
expect(canUseAiScan(ent)).toBe(true);
expect(canUseAiScan({ ...ent, aiScansUsedThisMonth: 5 })).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");
});
});