// Unit tests for the extracted licensing policy/entitlement rules and for LicenseService running // against an injected in-memory store (including files written before the key/cloud split). import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Feature } from '@d3ro/core/types' import type { LicenseVerificationResult } from '@d3ro/core/utils/crypto-license' import { generateLicenseKeyPair, issueSignedLicenseKey } from '@d3ro/core/utils/crypto-license' import { higherTier, quotaLimitFor, tierAtLeast, upgradeTargetFor, buildTierComparison, } from '../../../src/main/services/license/license-policy' import { composeLicenseInfo, resolveCachedCloudTier, resolveStoredKey, type StoredKeyRecord, } from '../../../src/main/services/license/license-entitlement' import { MemoryLicenseStore } from '../../../src/main/services/license/license-store' import type { UsageRepository } from '../../../src/main/services/license/usage-repository' import { LicenseService } from '../../../src/main/services/LicenseService' const DAY_MS = 24 * 60 * 60 * 1000 const T0 = new Date('2026-03-01T00:00:00Z').getTime() const EMPTY_RECORD: StoredKeyRecord = { licenseKey: null, keyTier: null, activatedAt: null, customerEmail: null, trialExpiresAt: null, } function verification(overrides: Partial): LicenseVerificationResult { return { valid: true, tier: 'pro', reason: 'valid', payload: null, message: 'ok', ...overrides } } const noUsage: UsageRepository = { getCount: () => 0, increment: () => undefined } describe('license-policy', () => { it('orders tiers and picks the higher one', () => { expect(tierAtLeast('pro_plus', 'pro')).toBe(true) expect(tierAtLeast('free', 'pro')).toBe(false) expect(higherTier('pro', 'free')).toBe('pro') expect(higherTier('free', 'pro_plus')).toBe('pro_plus') expect(higherTier('team', 'pro')).toBe('team') }) it('suggests the next paid tier on quota exhaustion', () => { expect(upgradeTargetFor('free')).toBe('pro') expect(upgradeTargetFor('pro')).toBe('pro_plus') }) it('only PREMIUM_LLM carries a quota and local features stay unlimited', () => { expect(quotaLimitFor('free', Feature.DICTATION)).toBeUndefined() expect(typeof quotaLimitFor('free', Feature.PREMIUM_LLM)).toBe('number') }) it('returns a fresh comparison table per call', () => { const a = buildTierComparison() const b = buildTierComparison() expect(a).toEqual(b) expect(a).not.toBe(b) expect(a).toHaveLength(17) }) }) describe('license-entitlement.resolveStoredKey', () => { it('returns none without a stored key', () => { expect(resolveStoredKey(EMPTY_RECORD, T0, () => verification({})).status).toBe('none') }) it('re-verifies a signed key instead of applying a grace window', () => { const verify = vi.fn(() => verification({ tier: 'pro_plus', payload: null })) const res = resolveStoredKey({ ...EMPTY_RECORD, licenseKey: 'D3RO-LIC-x', activatedAt: 1 }, T0, verify) expect(verify).toHaveBeenCalledWith('D3RO-LIC-x') expect(res).toMatchObject({ status: 'active', entitlement: { tier: 'pro_plus', activatedAt: 1 } }) }) it('reports a key whose own expiry passed as expired', () => { const res = resolveStoredKey({ ...EMPTY_RECORD, licenseKey: 'D3RO-LIC-x' }, T0, () => verification({ valid: false, reason: 'expired', tier: 'free' }), ) expect(res).toEqual({ status: 'expired', kind: 'license' }) }) it('reports other verification failures as unverified (the key is kept)', () => { const res = resolveStoredKey({ ...EMPTY_RECORD, licenseKey: 'D3RO-LIC-x' }, T0, () => verification({ valid: false, reason: 'invalid_signature', tier: 'free', message: 'no key' }), ) expect(res).toEqual({ status: 'unverified', message: 'no key' }) }) it('handles the local reverse trial by its own expiry without calling the verifier', () => { const verify = vi.fn(() => verification({})) const record = { ...EMPTY_RECORD, licenseKey: 'TRIAL-PRO-PLUS-abc', trialExpiresAt: T0 + DAY_MS } expect(resolveStoredKey(record, T0, verify)).toMatchObject({ status: 'active', entitlement: { tier: 'pro_plus', isTrial: true }, }) expect(resolveStoredKey(record, T0 + 2 * DAY_MS, verify)).toEqual({ status: 'expired', kind: 'trial' }) expect(verify).not.toHaveBeenCalled() }) }) describe('license-entitlement.resolveCachedCloudTier', () => { const activePro = { status: 'active' as const, entitlement: { tier: 'pro' as const, licenseKey: 'k', activatedAt: null, expiresAt: null, customerEmail: null, isTrial: false, trialExpiresAt: null, }, } it('prefers the explicitly stored cloud tier', () => { expect(resolveCachedCloudTier('pro', 'free', { status: 'none' })).toBe('pro') }) it('recovers a legacy cloud tier only when no key explains it', () => { expect(resolveCachedCloudTier(null, 'pro', { status: 'none' })).toBe('pro') expect(resolveCachedCloudTier(null, 'pro', activePro)).toBe('free') expect(resolveCachedCloudTier(null, 'pro_plus', activePro)).toBe('pro_plus') expect(resolveCachedCloudTier(null, 'pro', { status: 'unverified', message: '' })).toBe('free') expect(resolveCachedCloudTier(null, null, { status: 'none' })).toBe('free') }) it('composes the effective tier as max(key, cloud)', () => { const info = composeLicenseInfo({ machineId: 'm', key: activePro.entitlement, keyVerifiedAt: 5, cloudTier: 'free', cloudVerifiedAt: 9, }) expect(info.tier).toBe('pro') expect(info.lastVerifiedAt).toBe(9) expect(info.offlineGraceUntil).toBeNull() }) }) describe('LicenseService with an injected store', () => { let previousPublicKey: string | undefined let signedKey = '' beforeEach(() => { previousPublicKey = process.env.D3RO_LICENSE_PUBLIC_KEY vi.useFakeTimers() vi.setSystemTime(T0) const { publicKeyPem, privateKeyPem } = generateLicenseKeyPair() process.env.D3RO_LICENSE_PUBLIC_KEY = publicKeyPem signedKey = issueSignedLicenseKey( { licenseId: 'lic-legacy', tier: 'pro', customerEmail: 'legacy@example.test', issuedAt: T0 - 90 * DAY_MS, expiresAt: T0 + 275 * DAY_MS, machineId: null, }, privateKeyPem, ) }) afterEach(() => { vi.useRealTimers() if (previousPublicKey === undefined) delete process.env.D3RO_LICENSE_PUBLIC_KEY else process.env.D3RO_LICENSE_PUBLIC_KEY = previousPublicKey }) it('restores a legacy file whose 30-day grace already ran out', () => { const store = new MemoryLicenseStore() store.writeAll({ licenseTier: 'pro', licenseKey: signedKey, licenseActivatedAt: T0 - 90 * DAY_MS, licenseOfflineGraceUntil: T0 - 60 * DAY_MS, }) const svc = new LicenseService(store, noUsage) svc.initialize() expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(signedKey) expect(store.read('licenseCloudTier')).toBe('free') }) it('restores a key that a free cloud sync had hidden in a legacy file', () => { const store = new MemoryLicenseStore() store.writeAll({ licenseTier: 'free', licenseKey: signedKey }) const svc = new LicenseService(store, noUsage) svc.initialize() expect(svc.tier).toBe('pro') expect(store.read('licenseTier')).toBe('pro') }) it('keeps a legacy cached cloud tier until sign-out', () => { const store = new MemoryLicenseStore() store.writeAll({ licenseTier: 'pro_plus', licenseLastVerifiedAt: T0 - DAY_MS }) const svc = new LicenseService(store, noUsage) svc.initialize() expect(svc.tier).toBe('pro_plus') expect(svc.getInfo().lastVerifiedAt).toBe(T0 - DAY_MS) svc.resetToFree() expect(svc.tier).toBe('free') expect(store.read('licenseCloudTier')).toBe('free') }) it('keeps a legacy reverse trial until its expiry', () => { const store = new MemoryLicenseStore() store.writeAll({ licenseTier: 'pro_plus', licenseKey: 'TRIAL-PRO-PLUS-abcdef12', licenseIsTrial: true, licenseTrialExpiresAt: T0 + 3 * DAY_MS, licenseTrialEverStarted: true, }) const svc = new LicenseService(store, noUsage) svc.initialize() expect(svc.tier).toBe('pro_plus') expect(svc.getInfo().isTrial).toBe(true) vi.setSystemTime(T0 + 4 * DAY_MS) svc.initialize() expect(svc.tier).toBe('free') expect(store.read('licenseKey')).toBeNull() expect(store.read('licenseTrialEverStarted')).toBe(true) }) it('records quota usage through the injected repository', () => { const increment = vi.fn() const svc = new LicenseService(new MemoryLicenseStore(), { getCount: () => 0, increment }) svc.initialize() svc.consumeQuota(Feature.DICTATION) expect(increment).not.toHaveBeenCalled() }) })