import { describe, it, expect } from "vitest"; // Subject lives in src/shared/entitlements.ts; test co-located here per the // issue's file plan (services/). import { isEntitled, requiredTierFor, ENTITLEMENTS, type FeatureKey, } from "../shared/entitlements"; import type { Edition } from "./licenseService"; import fr from "../i18n/locales/fr.json"; import en from "../i18n/locales/en.json"; const EDITIONS: Edition[] = ["free", "base", "premium"]; const FEATURES = Object.keys(ENTITLEMENTS) as FeatureKey[]; describe("isEntitled — matrix", () => { it("denies every feature in Free (fail-closed)", () => { FEATURES.forEach((f) => { expect(isEntitled(f, "free", [])).toBe(false); }); }); it("Base unlocks budget/adjustments/reports-advanced/multi-profile, not balance", () => { expect(isEntitled("budget", "base", [])).toBe(true); expect(isEntitled("adjustments", "base", [])).toBe(true); expect(isEntitled("reports-advanced", "base", [])).toBe(true); expect(isEntitled("multi-profile", "base", [])).toBe(true); expect(isEntitled("balance", "base", [])).toBe(false); }); it("Premium unlocks everything including balance", () => { FEATURES.forEach((f) => { expect(isEntitled(f, "premium", [])).toBe(true); }); }); it("matches the ENTITLEMENTS table for every feature × edition", () => { FEATURES.forEach((f) => { EDITIONS.forEach((ed) => { const expected = ed !== "free" && ENTITLEMENTS[f].includes(ed); expect(isEntitled(f, ed, [])).toBe(expected); }); }); }); }); describe("isEntitled — features[] override", () => { it("grants a higher-tier feature when the signed override lists it", () => { // balance is Premium-only; a Base license carrying balance in features[] gets it. expect(isEntitled("balance", "base", ["balance"])).toBe(true); }); it("IGNORES the override when edition is free (CWE-863)", () => { // A key that resolves to free (invalid or expired) still lists its signed // features[]. Those must not be re-granted. expect(isEntitled("balance", "free", ["balance"])).toBe(false); expect(isEntitled("budget", "free", ["budget"])).toBe(false); }); it("does not grant unrelated features via the override", () => { expect(isEntitled("balance", "base", ["budget"])).toBe(false); }); }); describe("isEntitled — unknown feature", () => { it("denies an unknown feature key (deny-all)", () => { expect(isEntitled("web-sync" as FeatureKey, "premium", [])).toBe(false); expect(isEntitled("web-sync" as FeatureKey, "base", [])).toBe(false); }); it("still honours a signed override for an unknown key when non-free", () => { expect(isEntitled("web-sync" as FeatureKey, "premium", ["web-sync"])).toBe(true); }); it("keeps an unknown feature denied in Free even with an override", () => { expect(isEntitled("web-sync" as FeatureKey, "free", ["web-sync"])).toBe(false); }); }); describe("requiredTierFor — upsell tier derivation", () => { it("maps every Base-tier feature to base, and balance to premium", () => { expect(requiredTierFor("budget")).toBe("base"); expect(requiredTierFor("adjustments")).toBe("base"); expect(requiredTierFor("reports-advanced")).toBe("base"); expect(requiredTierFor("multi-profile")).toBe("base"); expect(requiredTierFor("balance")).toBe("premium"); }); it("returns the MINIMUM tier satisfying isEntitled, never free", () => { FEATURES.forEach((f) => { const tier = requiredTierFor(f); expect(tier).not.toBe("free"); // The returned tier does unlock the feature… expect(isEntitled(f, tier, [])).toBe(true); // …and is minimal: premium only when base does not unlock it. if (tier === "premium") { expect(isEntitled(f, "base", [])).toBe(false); } }); }); }); describe("upsell i18n coverage (fr + en)", () => { // UpsellGate renders t(`upsell.features.${feature}`) — every FeatureKey must // resolve in BOTH locales or a locked screen shows a raw key to the user. // Structural sub-type only: full typeof-identity between the two JSON files // is not this test's concern. interface UpsellMessages { nav: { locked: string }; license: { editions: { base: string; premium: string } }; upsell: { title: string; features: Record; ctaGet: string; premiumNotForSale: string; ctaHaveKey: string; revoked: string; }; } const locales: Record = { fr, en }; it("has a non-empty upsell description for every FeatureKey in both locales", () => { Object.entries(locales).forEach(([lng, messages]) => { FEATURES.forEach((f) => { expect( messages.upsell.features[f], `missing upsell.features.${f} in ${lng}`, ).toBeTruthy(); }); }); }); it("has no stale upsell.features key outside the ENTITLEMENTS matrix", () => { Object.entries(locales).forEach(([lng, messages]) => { Object.keys(messages.upsell.features).forEach((k) => { expect(FEATURES, `stale upsell.features.${k} in ${lng}`).toContain(k); }); }); }); it("exposes the title, CTA keys and nav.locked in both locales", () => { Object.values(locales).forEach((messages) => { expect(messages.upsell.title).toBeTruthy(); expect(messages.upsell.ctaGet).toBeTruthy(); expect(messages.upsell.premiumNotForSale).toBeTruthy(); expect(messages.upsell.ctaHaveKey).toBeTruthy(); expect(messages.upsell.revoked).toBeTruthy(); expect(messages.nav.locked).toBeTruthy(); }); }); // Pinning the whole key set catches a stale CTA key coming back in either // locale (the Premium note replaced the "coming soon" note in #346). it("has exactly the upsell keys the locked screen uses, in both locales", () => { Object.entries(locales).forEach(([lng, messages]) => { expect(Object.keys(messages.upsell).sort(), lng).toEqual([ "ctaGet", "ctaHaveKey", "features", "premiumNotForSale", "revoked", "title", ]); }); }); it("keeps the license.editions. label keys required by the CTA", () => { Object.values(locales).forEach((messages) => { expect(messages.license.editions.base).toBeTruthy(); expect(messages.license.editions.premium).toBeTruthy(); }); }); }); // Texts decided in planning (#346): the Premium note (D3), the Compte Maximus // card (D4) and the download link of the free edition (D11), pinned verbatim. describe("purchase path texts (fr + en)", () => { interface PurchaseTexts { upsell: { premiumNotForSale: string }; account: { description: string }; settings: { updates: { downloadLatest: string; notEntitled: string } }; } const texts = { fr, en } as unknown as Record<"fr" | "en", PurchaseTexts>; it("says Premium is not sold online yet", () => { expect(texts.fr.upsell.premiumNotForSale).toBe("Premium n'est pas encore offert en ligne."); expect(texts.en.upsell.premiumNotForSale).toBe("Premium isn't sold online yet."); }); it("describes what the account is for, without promising Premium", () => { expect(texts.fr.account.description).toBe( "Le compte sert à retrouver vos licences sur lacompagniemaximus.com et à vous identifier dans vos rétroactions. Les éditions Base et Premium viennent de la clé de licence, pas du compte.", ); expect(texts.en.account.description).toBe( "Your account lets you find your licenses on lacompagniemaximus.com and identifies you in the feedback you send. Base and Premium editions come from your license key, not from the account.", ); }); it("labels the download link of the free edition", () => { expect(texts.fr.settings.updates.downloadLatest).toBe("Télécharger la dernière version"); expect(texts.en.settings.updates.downloadLatest).toBe("Download the latest version"); }); it("tells the free edition that automatic updates come with Base", () => { expect(texts.fr.settings.updates.notEntitled).toBe( "Les mises à jour automatiques sont incluses dans l'édition Base.", ); expect(texts.en.settings.updates.notEntitled).toBe( "Automatic updates are included in the Base edition.", ); }); });