// Regression: a signed offline license key must survive (a) app restarts more than // 30 days after activation and (b) a cloud sign-in followed by sign-out. // A "restart" is simulated by calling initialize() again on the same service, // which reloads every field from the license store. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { generateLicenseKeyPair, issueSignedLicenseKey } from '@d3ro/core/utils/crypto-license' import { getLicenseService, resetLicenseServiceForTests, } from '../../../src/main/services/LicenseService' const DAY_MS = 24 * 60 * 60 * 1000 const T0 = new Date('2026-01-01T00:00:00Z').getTime() let previousPublicKey: string | undefined /** issueKey() generates a fresh key pair per call; remember the public key per issued key. */ const publicKeyByLicense = new Map() function previousKeyFor(key: string): string { const pem = publicKeyByLicense.get(key) if (!pem) throw new Error('unknown key') return pem } function issueKey(tier: 'pro' | 'pro_plus', expiresAt: number | null): string { const { publicKeyPem, privateKeyPem } = generateLicenseKeyPair() process.env.D3RO_LICENSE_PUBLIC_KEY = publicKeyPem const key = issueSignedLicenseKey( { licenseId: `lic-r1-11-${tier}`, tier, customerEmail: 'buyer@example.test', issuedAt: T0, expiresAt, machineId: null, }, privateKeyPem, ) publicKeyByLicense.set(key, publicKeyPem) return key } beforeEach(() => { previousPublicKey = process.env.D3RO_LICENSE_PUBLIC_KEY vi.useFakeTimers() vi.setSystemTime(T0) resetLicenseServiceForTests() }) afterEach(() => { vi.useRealTimers() if (previousPublicKey === undefined) delete process.env.D3RO_LICENSE_PUBLIC_KEY else process.env.D3RO_LICENSE_PUBLIC_KEY = previousPublicKey resetLicenseServiceForTests() }) describe('LicenseService — signed offline key lifetime (redteam r1-11)', () => { it('keeps a one-year pro key after a restart 31 days after activation', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() const result = await svc.activate(key) expect(result.success).toBe(true) vi.setSystemTime(T0 + 31 * DAY_MS) svc.initialize() expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(key) expect(svc.getInfo().expiresAt).toBe(T0 + 365 * DAY_MS) }) it('keeps a perpetual key after a restart a year later', async () => { const key = issueKey('pro_plus', null) const svc = getLicenseService() svc.initialize() await svc.activate(key) vi.setSystemTime(T0 + 400 * DAY_MS) svc.initialize() expect(svc.tier).toBe('pro_plus') expect(svc.getInfo().licenseKey).toBe(key) }) it('drops the key once its own expiresAt has passed', async () => { const key = issueKey('pro', T0 + 60 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) vi.setSystemTime(T0 + 61 * DAY_MS) svc.initialize() expect(svc.tier).toBe('free') expect(svc.getInfo().licenseKey).toBeNull() }) it('a free cloud account does not demote the key, before or after a restart', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) svc.syncFromCloud('free') expect(svc.tier).toBe('pro') svc.initialize() expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(key) }) it('sign-out after a pro cloud account keeps the key and its tier', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) svc.syncFromCloud('pro_plus') expect(svc.tier).toBe('pro_plus') svc.resetToFree() expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(key) svc.initialize() expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(key) }) it('a higher cloud tier wins over the key and survives a restart until sign-out', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) svc.syncFromCloud('pro_plus') svc.initialize() expect(svc.tier).toBe('pro_plus') svc.resetToFree() expect(svc.tier).toBe('pro') }) it('emits tier-changed only when the effective tier changes', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) const tiers: string[] = [] svc.on('tier-changed', (info: { tier: string }) => tiers.push(info.tier)) svc.syncFromCloud('free') svc.syncFromCloud('pro') svc.syncFromCloud('pro_plus') svc.resetToFree() expect(tiers).toEqual(['pro_plus', 'pro']) }) it('deactivate clears the key but keeps the cloud tier', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) svc.syncFromCloud('pro_plus') await svc.deactivate() expect(svc.getInfo().licenseKey).toBeNull() expect(svc.tier).toBe('pro_plus') svc.resetToFree() expect(svc.tier).toBe('free') }) it('does not erase a stored key it cannot verify at startup (e.g. verification key missing)', async () => { const key = issueKey('pro', T0 + 365 * DAY_MS) const svc = getLicenseService() svc.initialize() await svc.activate(key) // Verification key rotated / missing: the key is not honoured, but it stays on disk. const { publicKeyPem } = generateLicenseKeyPair() process.env.D3RO_LICENSE_PUBLIC_KEY = publicKeyPem svc.initialize() expect(svc.tier).toBe('free') // Restored verification key: the same stored key is honoured again. process.env.D3RO_LICENSE_PUBLIC_KEY = previousKeyFor(key) svc.initialize() expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(key) }) it('a reverse trial still ends after 14 days', () => { const svc = getLicenseService() svc.initialize() expect(svc.startTrial('trial@example.test').success).toBe(true) expect(svc.tier).toBe('pro_plus') vi.setSystemTime(T0 + 13 * DAY_MS) svc.initialize() expect(svc.tier).toBe('pro_plus') vi.setSystemTime(T0 + 15 * DAY_MS) svc.initialize() expect(svc.tier).toBe('free') expect(svc.getInfo().isTrial).toBe(false) expect(svc.startTrial('again@example.test').success).toBe(false) }) })