import { describe, it, expect, vi, beforeEach } from "vitest"; import { invoke } from "@tauri-apps/api/core"; import { parseLicenseError, LICENSE_ERROR_I18N_MAP, activateMachine, deactivateMachine, listActivatedMachines, type LicenseErrorCode, } from "./licenseService"; import fr from "../i18n/locales/fr.json"; import en from "../i18n/locales/en.json"; vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(), })); const mockInvoke = vi.mocked(invoke); // Codes the Rust `LicenseError` enum serializes (license_commands.rs), each // with the message key written out literally. Comparing against // LICENSE_ERROR_I18N_MAP itself would be circular: swapping two entries of the // map (an expired key shown as "revoked") would still pass. const EXPECTED_KEYS: [Exclude, string][] = [ ["license_not_found", "license.errors.licenseNotFound"], ["license_revoked", "license.errors.licenseRevoked"], ["license_expired", "license.errors.licenseExpired"], ["machine_limit_reached", "license.errors.machineLimitReached"], ["no_active_activation", "license.errors.noActiveActivation"], ["invalid_input", "license.errors.invalidInput"], ["invalid_prefix", "license.errors.invalidPrefix"], ["invalid_signature", "license.errors.invalidSignature"], ["malformed", "license.errors.malformed"], ["unknown_edition", "license.errors.unknownEdition"], ["network", "license.errors.network"], ["invalid_response", "license.errors.invalidResponse"], ["server", "license.errors.server"], ["no_license_stored", "license.errors.noLicenseStored"], ["storage", "license.errors.storage"], ["internal", "license.errors.internal"], ]; describe("parseLicenseError", () => { it.each(EXPECTED_KEYS)("maps %s to %s", (code, key) => { const err = parseLicenseError(JSON.stringify({ code })); expect(err.code).toBe(code); expect(err.i18nKey).toBe(key); expect(err.params).toBeUndefined(); }); it("gives every code its own message key", () => { const keys = Object.values(LICENSE_ERROR_I18N_MAP); expect(new Set(keys).size).toBe(keys.length); }); it("keeps the server status payload without breaking the mapping", () => { const err = parseLicenseError('{"code":"server","status":502}'); expect(err.code).toBe("server"); expect(err.i18nKey).toBe("license.errors.server"); }); it("carries the retry delay of a rate limit as the plural count", () => { const err = parseLicenseError('{"code":"rate_limit","retry_after_s":42}'); expect(err).toEqual({ code: "rate_limit", i18nKey: "license.errors.rateLimit", params: { count: 42 }, }); }); it("falls back to the server's 60 s window when retry_after_s is missing or unusable", () => { for (const body of [ '{"code":"rate_limit"}', '{"code":"rate_limit","retry_after_s":0}', '{"code":"rate_limit","retry_after_s":"soon"}', ]) { expect(parseLicenseError(body).params).toEqual({ count: 60 }); } }); it("returns unknown for anything that is not a serialized LicenseError", () => { for (const e of [ "Activation failed", '{"code":"not_a_license_code"}', '{"error":{"code":"license_revoked"}}', "null", "42", new Error("boom"), undefined, null, ]) { const err = parseLicenseError(e); expect(err.code).toBe("unknown"); expect(err.i18nKey).toBe("license.errors.unknown"); } }); }); describe("license command wrappers", () => { beforeEach(() => { mockInvoke.mockReset(); }); it("returns ok with the value when the command resolves", async () => { const machines = [ { machine_id: "m1", machine_name: "laptop", activated_at: "2026-09-01T00:00:00Z", last_seen_at: "2026-09-20T00:00:00Z", is_this_machine: true, }, ]; mockInvoke.mockResolvedValueOnce(machines); await expect(listActivatedMachines()).resolves.toEqual({ ok: true, value: machines }); expect(mockInvoke).toHaveBeenCalledWith("list_activated_machines", undefined); }); it("returns a typed error when the command rejects with a LicenseError", async () => { mockInvoke.mockRejectedValueOnce('{"code":"machine_limit_reached"}'); const result = await activateMachine(); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.code).toBe("machine_limit_reached"); expect(result.error.i18nKey).toBe("license.errors.machineLimitReached"); } }); it("passes the machine id to deactivate_machine", async () => { mockInvoke.mockResolvedValueOnce(undefined); await expect(deactivateMachine("m2")).resolves.toEqual({ ok: true, value: undefined }); expect(mockInvoke).toHaveBeenCalledWith("deactivate_machine", { machineId: "m2" }); }); }); // The render tests echo i18n keys instead of translating them: a key missing // from a locale would show up as a raw "license.errors.x" string with the // suite green. Only these checks read the real locale files. describe("license i18n coverage (fr + en)", () => { const locales: Record = { fr, en }; const resolve = (messages: unknown, key: string): unknown => key .split(".") .reduce( (node, part) => node && typeof node === "object" ? (node as Record)[part] : undefined, messages, ); // i18next resolves a key used with `count` through its plural forms. const PLURAL_KEYS = new Set(["license.errors.rateLimit"]); it("resolves every mapped key to a non-empty string in both locales", () => { for (const [lng, messages] of Object.entries(locales)) { for (const key of Object.values(LICENSE_ERROR_I18N_MAP)) { const variants = PLURAL_KEYS.has(key) ? [`${key}_one`, `${key}_other`] : [key]; for (const variant of variants) { const value = resolve(messages, variant); expect(typeof value, `${variant} in ${lng}`).toBe("string"); expect((value as string).trim(), `${variant} in ${lng}`).not.toBe(""); } } } }); it("puts the {{count}} placeholder in both plural forms of the rate limit message", () => { for (const [lng, messages] of Object.entries(locales)) { for (const form of ["one", "other"]) { expect(resolve(messages, `license.errors.rateLimit_${form}`), `${form} in ${lng}`).toContain( "{{count}}", ); } } }); // Keys of the License card outside the error map (#347). const CARD_KEYS = [ "license.noExpiry", "license.machines.count", "license.machines.activating", "license.machines.deactivating", "license.machines.notActivated", ]; it("resolves every License card key to a non-empty string in both locales", () => { for (const [lng, messages] of Object.entries(locales)) { for (const key of CARD_KEYS) { const value = resolve(messages, key); expect(typeof value, `${key} in ${lng}`).toBe("string"); expect((value as string).trim(), `${key} in ${lng}`).not.toBe(""); } } }); it("interpolates both numbers of the machine counter", () => { for (const [lng, messages] of Object.entries(locales)) { const counter = resolve(messages, "license.machines.count"); expect(counter, lng).toContain("{{count}}"); expect(counter, lng).toContain("{{limit}}"); } }); it("no longer carries the keys #347 removed", () => { for (const [lng, messages] of Object.entries(locales)) { for (const key of ["license.removeLicense", "license.machines.activateError"]) { expect(resolve(messages, key), `${key} in ${lng}`).toBeUndefined(); } } }); });