const path = require('path'); const fs = require('fs'); function makeCreds() { return { values: {}, store: jest.fn(async function(key, value) { this.values[key] = value; }), retrieve: jest.fn(async function(key) { return this.values[key] || null; }), delete: jest.fn(async function(key) { delete this.values[key]; }), }; } describe('server-managed stable license contract', () => { const previous = process.env.LICENSE_SERVER_URL; beforeEach(() => { jest.resetModules(); process.env.LICENSE_SERVER_URL = 'https://licenses.dashcaddy.net'; try { fs.unlinkSync('/tmp/dc-license-contract-config.json.license-revoked'); } catch (_) { /* absent */ } }); afterAll(() => { if (previous === undefined) delete process.env.LICENSE_SERVER_URL; else process.env.LICENSE_SERVER_URL = previous; }); test('refresh keeps the same key while accepting an extended server expiry', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; manager.activation = { code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), features: ['sso'], validationMethod: 'online', }; const extendedExpiry = new Date(Date.now() + 90 * 86400000).toISOString(); manager._validateOnline = jest.fn().mockResolvedValue({ success: true, activation: { code, durationDays: 90, expiresAt: extendedExpiry, features: ['sso', 'recipes', 'swarm'], }, }); manager._updateConfig = jest.fn().mockResolvedValue(); expect(await manager.refreshOnline(true)).toBe(true); expect(manager.activation.code).toBe(code); expect(manager.activation.expiresAt).toBe(extendedExpiry); expect(manager.activation.validationMethod).toBe('online'); expect(creds.store).toHaveBeenCalled(); }); test('server outage does not create a fresh offline activation', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); manager._validateOnline = jest.fn().mockResolvedValue(null); const result = await manager.activate('DC-20F00-00059-JN7W8-0SW2N-MA200'); expect(result.success).toBe(false); expect(result.message).toMatch(/temporarily unavailable/); expect(manager.activation).toBeNull(); }); test('background timer forces refresh every 15 minutes', async () => { jest.useFakeTimers(); try { const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); manager.activation = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), }; manager.refreshOnline = jest.fn().mockResolvedValue(true); manager._startOnlineRefresh(); await jest.advanceTimersByTimeAsync(15 * 60 * 1000); expect(manager.refreshOnline).toHaveBeenCalledWith(true); clearInterval(manager._onlineRefreshTimer); } finally { jest.useRealTimers(); } }); test('explicit server rejection revokes cached entitlement', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); manager.activation = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'online', }; manager._validateOnline = jest.fn().mockResolvedValue({ success: false, message: 'License revoked' }); manager._updateConfig = jest.fn().mockResolvedValue(); expect(await manager.refreshOnline(true)).toBe(false); expect(manager.activation).toBeNull(); expect(creds.delete).toHaveBeenCalledWith('license.activation'); }); test('server outage never trusts a legacy offline cache as server-managed', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; manager.activation = { code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'offline', }; manager._validateOnline = jest.fn().mockResolvedValue(null); const result = await manager.activate(code); expect(result.success).toBe(false); expect(result.message).toMatch(/temporarily unavailable/); }); test('startup quarantines a stored legacy offline entitlement during outage', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; creds.values['license.activation'] = JSON.stringify({ code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'offline', }); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); manager._validateOnline = jest.fn().mockResolvedValue(null); manager._updateConfig = jest.fn().mockResolvedValue(); await manager.load(); expect(manager.activation).toBeNull(); expect(creds.delete).toHaveBeenCalledWith('license.activation'); }); test('activation-time explicit rejection revokes matching cached entitlement', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); manager.activation = { code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'online', }; manager._validateOnline = jest.fn().mockResolvedValue({ success: false, message: 'Revoked' }); manager._updateConfig = jest.fn().mockResolvedValue(); const result = await manager.activate(code); expect(result.success).toBe(false); expect(manager.activation).toBeNull(); expect(creds.delete).toHaveBeenCalledWith('license.activation'); }); test('deactivate during refresh cannot resurrect entitlement', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; manager.activation = { code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'online' }; let release; manager._validateOnline = jest.fn(() => new Promise(resolve => { release = resolve; })); manager._updateConfig = jest.fn().mockResolvedValue(); manager._notifyDeactivation = jest.fn().mockResolvedValue(); const refresh = manager.refreshOnline(true); const deactivate = manager.deactivate(); await new Promise(resolve => setImmediate(resolve)); release({ success: true, activation: { code, durationDays: 90, expiresAt: new Date(Date.now() + 90 * 86400000).toISOString() } }); await refresh; expect((await deactivate).success).toBe(true); expect(manager.activation).toBeNull(); }); test('different-key activation waits for refresh and remains current', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); const oldCode = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; const newCode = 'DC-20F00-0005A-JN7W8-0SW2N-MA200'; manager.activation = { code: oldCode, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'online' }; let release; manager._validateOnline = jest.fn() .mockImplementationOnce(() => new Promise(resolve => { release = resolve; })) .mockResolvedValueOnce({ success: true, activation: { code: newCode, durationDays: 30, expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), features: ['sso'] } }); manager._updateConfig = jest.fn().mockResolvedValue(); const refresh = manager.refreshOnline(true); const activate = manager.activate(newCode); await new Promise(resolve => setImmediate(resolve)); release({ success: true, activation: { code: oldCode, durationDays: 90, expiresAt: new Date(Date.now() + 90 * 86400000).toISOString() } }); await refresh; expect((await activate).success).toBe(true); expect(manager.activation.code).toBe(newCode); }); test('concurrent activations commit in request order without stale overwrite', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); const firstCode = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; const secondCode = 'DC-20F00-0005A-JN7W8-0SW2N-MA200'; let releaseFirst; manager._validateOnline = jest.fn() .mockImplementationOnce(() => new Promise(resolve => { releaseFirst = resolve; })) .mockResolvedValueOnce({ success: true, activation: { code: secondCode, durationDays: 90, expiresAt: new Date(Date.now() + 90 * 86400000).toISOString(), features: ['sso'] } }); manager._updateConfig = jest.fn().mockResolvedValue(); const first = manager.activate(firstCode); const second = manager.activate(secondCode); await new Promise(resolve => setImmediate(resolve)); releaseFirst({ success: true, activation: { code: firstCode, durationDays: 30, expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), features: ['sso'] } }); expect((await first).success).toBe(true); expect((await second).success).toBe(true); expect(manager.activation.code).toBe(secondCode); }); test.each([429, 500, 502, 503])('retryable HTTP %i never revokes cached online entitlement', async (status) => { const originalFetch = global.fetch; try { global.fetch = jest.fn().mockResolvedValue({ ok: false, status, json: async () => ({ error: 'temporary failure' }), }); const { LicenseManager } = require('../src/managers/license-manager'); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); manager.activation = { code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), validationMethod: 'online', }; expect(await manager.refreshOnline(true)).toBe(false); expect(manager.activation.code).toBe(code); } finally { global.fetch = originalFetch; } }); test.each([ { durationDays: 30, features: ['sso'] }, { expiresAt: 'not-a-date', durationDays: 30, features: ['sso'] }, { expiresAt: new Date(Date.now() - 1000).toISOString(), durationDays: 30, features: ['sso'] }, { expiresAt: new Date(Date.now() + 86400000).toISOString(), durationDays: 0, features: ['sso'] }, { expiresAt: new Date(Date.now() + 86400000).toISOString(), durationDays: 30, features: 'sso' }, { expiresAt: new Date(Date.now() + 20 * 365 * 86400000).toISOString(), durationDays: 30, features: ['sso'] }, ])('malformed HTTP 200 success never creates an unbounded entitlement', async (payload) => { const originalFetch = global.fetch; try { global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ success: true, ...payload }), }); const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); const result = await manager.activate('DC-20F00-00059-JN7W8-0SW2N-MA200'); expect(result.success).toBe(false); expect(manager.activation).toBeNull(); } finally { global.fetch = originalFetch; } }); test.each([ { durationDays: 30, features: ['sso'] }, { expiresAt: 'bad-date', durationDays: 30, features: ['sso'] }, { expiresAt: new Date(Date.now() - 1000).toISOString(), durationDays: 30, features: ['sso'] }, { expiresAt: new Date(Date.now() + 20 * 365 * 86400000).toISOString(), durationDays: 30, features: ['sso'], activatedAt: new Date().toISOString() }, ])('startup outage rejects malformed cached online entitlement', async (cached) => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); creds.values['license.activation'] = JSON.stringify({ code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', validationMethod: 'online', ...cached, }); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); manager._validateOnline = jest.fn().mockResolvedValue(null); manager._updateConfig = jest.fn().mockResolvedValue(); await manager.load(); expect(manager.activation).toBeNull(); }); test('deactivate waiting on authoritative rejection does not dereference revoked state', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); manager.activation = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), features: ['sso'], validationMethod: 'online', }; let release; manager._validateOnline = jest.fn(() => new Promise(resolve => { release = resolve; })); manager._updateConfig = jest.fn().mockResolvedValue(); const refresh = manager.refreshOnline(true); const deactivate = manager.deactivate(); await new Promise(resolve => setImmediate(resolve)); release({ success: false, message: 'Revoked' }); await refresh; const result = await deactivate; expect(result.success).toBe(false); expect(manager.activation).toBeNull(); }); test('revocation tombstone prevents restart resurrection when credential deletion fails', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const configPath = path.join('/tmp', 'dc-license-contract-config.json'); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; const cached = { code, durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }; const creds = makeCreds(); creds.values['license.activation'] = JSON.stringify(cached); creds.delete = jest.fn().mockRejectedValue(new Error('keychain unavailable')); const first = new LicenseManager(creds, configPath, {}); first.activation = cached; first._validateOnline = jest.fn().mockResolvedValue({ success: false, message: 'Revoked' }); first._updateConfig = jest.fn().mockResolvedValue(); expect(await first.refreshOnline(true)).toBe(false); expect(fs.existsSync(`${configPath}.license-revoked`)).toBe(true); const restarted = new LicenseManager(creds, configPath, {}); restarted._validateOnline = jest.fn().mockResolvedValue(null); restarted._updateConfig = jest.fn().mockResolvedValue(); await restarted.load(); expect(restarted.activation).toBeNull(); expect(restarted._updateConfig).toHaveBeenCalled(); }); test('tombstone write failure still clears rejected entitlement in memory', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), { error: jest.fn(), warn: jest.fn() }); manager.activation = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }; manager._writeRevocationTombstone = jest.fn().mockRejectedValue(new Error('disk full')); manager._validateOnline = jest.fn().mockResolvedValue({ success: false, message: 'Revoked' }); manager._updateConfig = jest.fn().mockResolvedValue(); expect(await manager.refreshOnline(true)).toBe(false); expect(manager.activation).toBeNull(); expect(creds.delete).toHaveBeenCalledWith('license.activation'); }); test('corrupt tombstone fails closed during restart', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const configPath = path.join('/tmp', 'dc-license-contract-config.json'); fs.writeFileSync(`${configPath}.license-revoked`, '{partial', { mode: 0o600 }); const creds = makeCreds(); creds.values['license.activation'] = JSON.stringify({ code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }); const manager = new LicenseManager(creds, configPath, {}); manager._validateOnline = jest.fn().mockResolvedValue(null); await manager.load(); expect(manager.activation).toBeNull(); expect(manager._validateOnline).not.toHaveBeenCalled(); }); test('activation persistence failure rolls back in-memory premium access', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const creds = makeCreds(); creds.store = jest.fn().mockRejectedValue(new Error('keychain full')); const manager = new LicenseManager(creds, path.join('/tmp', 'dc-license-contract-config.json'), {}); const code = 'DC-20F00-00059-JN7W8-0SW2N-MA200'; manager._validateOnline = jest.fn().mockResolvedValue({ success: true, activation: { code, durationDays: 30, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), features: ['sso'], } }); const result = await manager.activate(code); expect(result.success).toBe(false); expect(manager.activation).toBeNull(); expect(manager.isPro()).toBe(false); expect(manager.hasFeature('sso')).toBe(false); }); test('combined revocation persistence failures cannot restore plaintext config backup', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const configPath = `/tmp/dc-combined-failure-${process.pid}.json`; const cached = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }; fs.writeFileSync(configPath, JSON.stringify({ licenseBackup: cached })); const creds = makeCreds(); creds.values['license.activation'] = JSON.stringify(cached); creds.delete = jest.fn().mockRejectedValue(new Error('keychain locked')); const manager = new LicenseManager(creds, configPath, {}); manager.activation = cached; manager._writeRevocationTombstone = jest.fn().mockRejectedValue(new Error('disk full')); manager._validateOnline = jest.fn().mockResolvedValue({ success: false, message: 'Revoked' }); manager._updateConfig = jest.fn().mockRejectedValue(new Error('config locked')); await manager.refreshOnline(true); expect(manager.activation).toBeNull(); await expect(creds.retrieve('license.activation')).resolves.not.toBeNull(); const restarted = new LicenseManager(creds, configPath, {}); restarted._validateOnline = jest.fn().mockResolvedValue(null); await restarted.load(); expect(restarted.activation).toBeNull(); fs.unlinkSync(configPath); }); test('ambiguous empty HTTP 200 preserves bounded cached entitlement', async () => { const originalFetch = global.fetch; try { global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) }); const { LicenseManager } = require('../src/managers/license-manager'); const manager = new LicenseManager(makeCreds(), path.join('/tmp', 'dc-license-contract-config.json'), {}); manager.activation = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }; expect(await manager.refreshOnline(true)).toBe(false); expect(manager.activation).not.toBeNull(); } finally { global.fetch = originalFetch; } }); test('startup outage fails closed and automatically recovers in the same process', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const configPath = path.join('/tmp', 'dc-license-contract-config.json'); const cached = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }; const creds = makeCreds(); creds.values['license.activation'] = JSON.stringify(cached); const unavailable = new LicenseManager(creds, configPath, {}); unavailable._validateOnline = jest.fn().mockResolvedValue(null); unavailable._updateConfig = jest.fn().mockResolvedValue(); await unavailable.load(); expect(unavailable.activation).toBeNull(); await expect(creds.retrieve('license.activation')).resolves.not.toBeNull(); expect(fs.existsSync(`${configPath}.license-revoked`)).toBe(false); unavailable._validateOnline = jest.fn().mockResolvedValue({ success: true, activation: { ...cached, expiresAt: new Date(Date.now() + 30 * 86400000).toISOString() } }); const recovered = await unavailable._retryStartupValidation(); expect(recovered).toBe(true); expect(unavailable.activation.code).toBe(cached.code); expect(unavailable.activation.validationMethod).toBe('online'); }); test('startup recovery persistence failure stays fail-closed and remains retryable', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const configPath = path.join('/tmp', 'dc-license-contract-config.json'); const cached = { code: 'DC-20F00-00059-JN7W8-0SW2N-MA200', durationDays: 30, expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), activatedAt: new Date().toISOString(), features: ['sso'], validationMethod: 'online', }; const creds = makeCreds(); creds.values['license.activation'] = JSON.stringify(cached); const manager = new LicenseManager(creds, configPath, {}); manager._validateOnline = jest.fn().mockResolvedValue(null); manager._updateConfig = jest.fn().mockResolvedValue(); await manager.load(); expect(manager.activation).toBeNull(); manager._validateOnline.mockResolvedValue({ success: true, activation: cached }); creds.store.mockRejectedValueOnce(new Error('credential disk full')); expect(await manager._retryStartupValidation()).toBe(false); expect(manager.activation).toBeNull(); expect(manager._pendingStartupCode).toBe(cached.code); const preserved = await creds.retrieve('license.activation'); manager._updateConfig.mockRejectedValueOnce(new Error('config disk full')); expect(await manager._retryStartupValidation()).toBe(false); expect(manager.activation).toBeNull(); expect(await creds.retrieve('license.activation')).toBe(preserved); expect(manager._pendingStartupCode).toBe(cached.code); expect(await manager._retryStartupValidation()).toBe(true); expect(manager.activation.code).toBe(cached.code); }); });