A refunded license was revoked on the server but kept working in the app: the edition is resolved offline from the key alone, and the flag added by #347 lasted the session only and never touched the edition. Rust - license.check, next to the key, holds what the license server last said about it: {key_sha256, attempted_at, revoked}. Never the key itself. A missing, unreadable or invalid file, or a state about another key, reads as "no state": not revoked, check due. - entitlements_from_dir resolves a revoked key to ("free", []), which also locks auto-update. read_license and store_license report the state. - check_license_revocation: POST /v1/licenses/verify with the product and the key only. Nothing is sent without a stored key that validates, nor within 24 h of the last attempt, which is written before the request. Fail-open: only "valid" lifts the state, only "revoked" sets it. - Activation, machine list and deactivation record a license_revoked answer under the key they sent. - fetch_price reads error.code of a 403 and names license_revoked instead of premium_required. It records nothing: the price route judges the license of the activation token, which the client cannot tie to the stored key, and 0.15.0 left the token of a previous key next to a new one (#367). Front - LicenseContext takes the revoked state from Rust (info.revoked); the session flag and markRevoked are gone. The check starts once per app start, writes its outcome to the in-app log, and reloads the context only when the state changed. A revoked key entered again stays Free. - UpsellGate and the License card name the revocation. Docs: ADR 0017 amendment (D13 replaced, the first unsolicited request and its bounds), architecture, user guide, price contract annex, changelogs. Refs #365 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
1.5 KiB
TypeScript
61 lines
1.5 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { useIsPremium } from "./useIsPremium";
|
|
|
|
// useIsPremium now reads LicenseContext (was: useLicense). Mock the context hook.
|
|
vi.mock("../contexts/LicenseContext", () => ({
|
|
useLicenseContext: vi.fn(),
|
|
}));
|
|
|
|
import { useLicenseContext } from "../contexts/LicenseContext";
|
|
|
|
const mockUseLicenseContext = vi.mocked(useLicenseContext);
|
|
|
|
describe("useIsPremium", () => {
|
|
it('returns true when edition is "premium"', () => {
|
|
mockUseLicenseContext.mockReturnValue({
|
|
status: "ready",
|
|
edition: "premium",
|
|
features: [],
|
|
info: null,
|
|
error: null,
|
|
validating: false,
|
|
validationError: null,
|
|
revoked: false,
|
|
refresh: vi.fn(),
|
|
submitKey: vi.fn(),
|
|
});
|
|
expect(useIsPremium()).toBe(true);
|
|
});
|
|
|
|
it('returns false when edition is "base"', () => {
|
|
mockUseLicenseContext.mockReturnValue({
|
|
status: "ready",
|
|
edition: "base",
|
|
features: [],
|
|
info: null,
|
|
error: null,
|
|
validating: false,
|
|
validationError: null,
|
|
revoked: false,
|
|
refresh: vi.fn(),
|
|
submitKey: vi.fn(),
|
|
});
|
|
expect(useIsPremium()).toBe(false);
|
|
});
|
|
|
|
it('returns false when edition is "free"', () => {
|
|
mockUseLicenseContext.mockReturnValue({
|
|
status: "ready",
|
|
edition: "free",
|
|
features: [],
|
|
info: null,
|
|
error: null,
|
|
validating: false,
|
|
validationError: null,
|
|
revoked: false,
|
|
refresh: vi.fn(),
|
|
submitKey: vi.fn(),
|
|
});
|
|
expect(useIsPremium()).toBe(false);
|
|
});
|
|
});
|