/** * Comprehensive tests for src/managers/license-manager.js * * This is the revenue validation path — an untested bug here could silently * break activation for every paying customer (DC-083 priority #1). * * Unlike license-tier-enforcement.test.js (which stubs _validateOffline and * only tests the isPro/tier-gating surface), these tests exercise the REAL * crypto flow end-to-end: generateCode(TEST_SECRET, ...) → activate(code) → * _validateOffline(code) → verifyCode(secret, code) → credential store. * * Coverage matrix: * - constructor + load(): credential-store primary, config-backup fallback, * no-license, credential-store error → config recovery, re-store * - activate(): real-code round-trip (30/90/180/365), already-activated * idempotency, invalid format, missing code, offline-validation failure, * LIFETIME rejection (prod) + acceptance (dev env), credential-store * save failure, config write * - activate() online path: server success, server unreachable → offline * fallback, server explicit rejection (no fallback) * - deactivate(): success, no-active-license, credential delete * - getStatus(): free tier, active premium, expired, lifetime * - hasFeature(): no-activation, active, expired, specific-feature * - isPro() / isExpired() / daysRemaining(): all branches * - getMachineFingerprint(): stable, hex format * - requirePremium() middleware: feature-available (next), feature-unavailable (403) * - loadSecret(): file-exists, file-missing, file-unreadable * - _validateOffline(): with-secret, no-secret structural-only, invalid-code, * unsupported-version (forged v2 payload) * - _updateConfig(): writes license + backup, clears on deactivation * - _maskCode(): standard, short, empty * - Full lifecycle: activate → status → deactivate, load-after-activate restore */ 'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); // Use the real keygen to generate cryptographically valid codes. const { generateCode, verifyCode } = require('../license-keygen'); const TEST_SECRET = 'a'.repeat(64); // 32 bytes hex — deterministic test secret function _tmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'dc-lm-test-')); } function _cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* best effort */ } } /** * Build a LicenseManager with a fresh module instance + env override. * Returns { mgr, restore, dir }. * * @param {Object} opts * @param {Object} opts.creds — credential store stub {store, retrieve, delete} * @param {Object} opts.env — process.env overrides (e.g. LICENSE_SERVER_URL) * @param {string} opts.secret — master secret to load via loadSecret() */ function _makeManager(opts = {}) { const { creds = {}, env = {}, secret = null } = opts; const prevEnv = { ...process.env }; // Object.assign copies undefined-valued keys as the string "undefined". // Delete env keys whose value is undefined so the production "unset" path // is exercised accurately. for (const [k, v] of Object.entries(env)) { if (v === undefined) { delete process.env[k]; } else { process.env[k] = v; } } // Force re-require so LICENSE_SERVER_URL is re-read. delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); const dir = _tmpDir(); const configFile = path.join(dir, 'config.json'); const secretFile = path.join(dir, '.license-secret'); const defaultCreds = { _store: {}, async store(key, val) { this._store[key] = val; }, async retrieve(key) { return this._store[key] || null; }, async delete(key) { delete this._store[key]; }, }; const mgr = new LicenseManager( creds._impl ? creds : defaultCreds, configFile, { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } ); if (secret) { fs.writeFileSync(secretFile, secret, 'utf8'); mgr.loadSecret(secretFile); } const restore = async () => { process.env = prevEnv; _cleanup(dir); }; return { mgr, restore, dir, configFile, secretFile }; } // ── generateCode helper: mint a real valid code for a given duration ────── function _mintCode(secret, durationDays, codeId = 1) { return generateCode(secret, durationDays, codeId); } // =========================================================================== // constructor + load() // =========================================================================== describe('LicenseManager: load()', () => { test('loads active license from credential store', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { // Pre-populate the credential store with a valid activation const code = _mintCode(TEST_SECRET, 30, 1); const activation = { code, codeId: 1, durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso', 'recipes', 'swarm'], }; await mgr.credentialManager.store('license.activation', JSON.stringify(activation)); await mgr.load(); expect(mgr.activation).toBeTruthy(); expect(mgr.activation.code).toBe(code); expect(mgr._loaded).toBe(true); expect(mgr.isPro()).toBe(true); } finally { await restore(); } }); test('logs expired license on load but keeps it', async () => { const { mgr, restore } = _makeManager(); try { const activation = { code: 'DC-TEST-EXPIRED', durationDays: 30, lifetime: false, activatedAt: '2020-01-01T00:00:00.000Z', expiresAt: '2020-02-01T00:00:00.000Z', machineId: 'test', validationMethod: 'offline', features: ['sso'], }; await mgr.credentialManager.store('license.activation', JSON.stringify(activation)); await mgr.load(); expect(mgr.activation).toBeTruthy(); expect(mgr.isExpired()).toBe(true); expect(mgr._loaded).toBe(true); } finally { await restore(); } }); test('falls back to config.json licenseBackup when credential store fails', async () => { const dir = _tmpDir(); try { const configFile = path.join(dir, 'config.json'); const activation = { code: 'DC-BACKUP-TEST', durationDays: 90, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 90 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; fs.writeFileSync(configFile, JSON.stringify({ licenseBackup: activation }, null, 2)); // Credential store that throws on retrieve const failCreds = { async retrieve() { throw new Error('encryption key changed'); }, async store() {}, async delete() {}, }; delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager(failCreds, configFile, { info: () => {}, warn: () => {} }); await mgr.load(); expect(mgr.activation).toBeTruthy(); expect(mgr.activation.code).toBe('DC-BACKUP-TEST'); expect(mgr._loaded).toBe(true); } finally { _cleanup(dir); } }); test('re-stores recovered license in credential manager after config backup restore', async () => { const dir = _tmpDir(); try { const configFile = path.join(dir, 'config.json'); const activation = { code: 'DC-RESTORE-TEST', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; fs.writeFileSync(configFile, JSON.stringify({ licenseBackup: activation }, null, 2)); const storeCalls = []; const failCreds = { async retrieve() { throw new Error('decryption failed'); }, async store(key, val) { storeCalls.push({ key, val }); }, async delete() {}, }; delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager(failCreds, configFile, { info: () => {}, warn: () => {} }); await mgr.load(); expect(storeCalls.length).toBe(1); expect(storeCalls[0].key).toBe('license.activation'); expect(JSON.parse(storeCalls[0].val).code).toBe('DC-RESTORE-TEST'); } finally { _cleanup(dir); } }); test('sets activation=null when no license anywhere', async () => { const { mgr, restore } = _makeManager(); try { await mgr.load(); expect(mgr.activation).toBeNull(); expect(mgr._loaded).toBe(true); } finally { await restore(); } }); test('sets activation=null when config.json has no licenseBackup', async () => { const dir = _tmpDir(); try { const configFile = path.join(dir, 'config.json'); fs.writeFileSync(configFile, JSON.stringify({ someOtherField: true }, null, 2)); const emptyCreds = { async retrieve() { return null; }, async store() {}, async delete() {}, }; delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager(emptyCreds, configFile, { info: () => {} }); await mgr.load(); expect(mgr.activation).toBeNull(); } finally { _cleanup(dir); } }); }); // =========================================================================== // activate() — the core revenue path // =========================================================================== describe('LicenseManager: activate() — real crypto round-trip', () => { test('activates a valid 30-day code via offline HMAC validation', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 100); const result = await mgr.activate(code); expect(result.success).toBe(true); expect(result.message).toMatch(/30 days/); expect(result.activation).toBeTruthy(); expect(result.activation.active).toBe(true); expect(result.activation.tier).toBe('premium'); expect(result.activation.durationDays).toBe(30); expect(result.activation.lifetime).toBe(false); expect(result.activation.daysRemaining).toBeGreaterThan(29); expect(result.activation.daysRemaining).toBeLessThanOrEqual(30); } finally { await restore(); } }); test('activates valid 90, 180, and 365-day codes', async () => { for (const duration of [90, 180, 365]) { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, duration, 200 + duration); const result = await mgr.activate(code); expect(result.success).toBe(true); expect(result.activation.durationDays).toBe(duration); expect(result.activation.daysRemaining).toBeGreaterThan(duration - 1); } finally { await restore(); } } }); test('persists activation to credential store after successful activation', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 300); await mgr.activate(code); const stored = await mgr.credentialManager.retrieve('license.activation'); expect(stored).toBeTruthy(); const parsed = JSON.parse(stored); expect(parsed.code).toBe(code); expect(parsed.durationDays).toBe(30); } finally { await restore(); } }); test('writes config.json with license info + backup after activation', async () => { const { mgr, restore, configFile } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 90, 400); await mgr.activate(code); const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); expect(config.license.active).toBe(true); expect(config.license.tier).toBe('premium'); expect(config.licenseBackup).toBeTruthy(); expect(config.licenseBackup.code).toBe(code); } finally { await restore(); } }); test('idempotent: activating the same valid code twice returns already-activated', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 500); const result1 = await mgr.activate(code); expect(result1.success).toBe(true); const result2 = await mgr.activate(code); expect(result2.success).toBe(true); expect(result2.message).toMatch(/already activated/i); } finally { await restore(); } }); test('rejects empty/null/undefined code', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { expect((await mgr.activate('')).success).toBe(false); expect((await mgr.activate(null)).success).toBe(false); expect((await mgr.activate(undefined)).success).toBe(false); expect((await mgr.activate(12345)).success).toBe(false); } finally { await restore(); } }); test('rejects code without DC- prefix', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const result = await mgr.activate('XYZ-ABCDE-FGHIJ-KLMNO-PQRST-UVWXY'); expect(result.success).toBe(false); expect(result.message).toMatch(/DC-/); } finally { await restore(); } }); test('rejects code with invalid HMAC signature (all-A base32, forged)', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const result = await mgr.activate('DC-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA'); // Structurally valid base32 (all 'A') parses fine but the HMAC computed // from the decoded payload won't match — offline validation fails. expect(result.success).toBe(false); } finally { await restore(); } }); test('rejects code with wrong HMAC signature (forged)', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { // Generate with a DIFFERENT secret, try to activate with TEST_SECRET const wrongSecret = 'b'.repeat(64); const forgedCode = _mintCode(wrongSecret, 30, 600); const result = await mgr.activate(forgedCode); expect(result.success).toBe(false); expect(result.message).toMatch(/invalid|forged|corrupted/i); } finally { await restore(); } }); test('LIFETIME code is rejected in production (ALLOW_LIFETIME_LICENSE unset)', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET, env: { ALLOW_LIFETIME_LICENSE: undefined }, }); try { const lifetimeCode = _mintCode(TEST_SECRET, 0, 700); // durationDays=0 → lifetime const result = await mgr.activate(lifetimeCode); expect(result.success).toBe(false); expect(result.message).toMatch(/lifetime/i); expect(result.message).toMatch(/not available/i); expect(mgr.activation).toBeNull(); } finally { await restore(); } }); test('LIFETIME code is accepted when ALLOW_LIFETIME_LICENSE=true', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET, env: { ALLOW_LIFETIME_LICENSE: 'true' }, }); try { const lifetimeCode = _mintCode(TEST_SECRET, 0, 800); const result = await mgr.activate(lifetimeCode); expect(result.success).toBe(true); expect(result.activation.lifetime).toBe(true); expect(result.activation.tier).toBe('premium'); expect(result.activation.expiresAt).toBeNull(); expect(result.activation.daysRemaining).toBeNull(); expect(mgr.isPro()).toBe(true); } finally { await restore(); } }); test('returns failure when credential store throws on save', async () => { const failCreds = { async store() { throw new Error('disk full'); }, async retrieve() { return null; }, async delete() {}, }; const { mgr, restore } = _makeManager({ secret: TEST_SECRET, creds: { _impl: true, ...failCreds }, }); try { const code = _mintCode(TEST_SECRET, 30, 900); const result = await mgr.activate(code); expect(result.success).toBe(false); expect(result.message).toMatch(/failed to save/i); } finally { await restore(); } }); test('normalizes lowercase code to uppercase', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 1000); const result = await mgr.activate(code.toLowerCase()); expect(result.success).toBe(true); expect(mgr.activation.code).toBe(code); // stored uppercase } finally { await restore(); } }); test('trims whitespace around code', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 1100); const result = await mgr.activate(` ${code} `); expect(result.success).toBe(true); } finally { await restore(); } }); }); // =========================================================================== // activate() — online validation path // // LICENSE_SERVER_URL is a module-level const read at require() time, so we // use jest.isolateModules to get a fresh module instance with the env var // set. The credential manager stub is created inside the isolation callback. // =========================================================================== describe('LicenseManager: activate() — online validation', () => { test('uses online server when LICENSE_SERVER_URL is set and returns success', async () => { const originalFetch = global.fetch; const dir = _tmpDir(); const prevUrl = process.env.LICENSE_SERVER_URL; process.env.LICENSE_SERVER_URL = 'https://license.test.example'; try { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ success: true, codeId: 999, durationDays: 365, expiresAt: new Date(Date.now() + 365 * 86400000).toISOString(), features: ['sso', 'recipes', 'swarm'], token: 'srv-token-abc', }), }); const creds = { _store: {}, async store(key, val) { this._store[key] = val; }, async retrieve(key) { return this._store[key] || null; }, async delete(key) { delete this._store[key]; }, }; let result; jest.isolateModules(() => { const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager(creds, path.join(dir, 'config.json'), { info: () => {}, warn: () => {}, error: () => {} }); // Run synchronously — activate is async but we capture the promise result = mgr.activate('DC-ABCDE-FGHIJ-KLMNO-PQRST-UVWXY'); }); const res = await result; expect(res.success).toBe(true); expect(res.activation.validationMethod).toBe('online'); expect(global.fetch).toHaveBeenCalledWith( 'https://license.test.example/api/license/validate', expect.objectContaining({ method: 'POST' }) ); } finally { if (prevUrl === undefined) delete process.env.LICENSE_SERVER_URL; else process.env.LICENSE_SERVER_URL = prevUrl; global.fetch = originalFetch; _cleanup(dir); } }); test('falls back to offline when server is unreachable (fetch throws)', async () => { const originalFetch = global.fetch; const dir = _tmpDir(); const prevUrl = process.env.LICENSE_SERVER_URL; process.env.LICENSE_SERVER_URL = 'https://license.test.example'; try { global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED')); const secretFile = path.join(dir, '.license-secret'); fs.writeFileSync(secretFile, TEST_SECRET, 'utf8'); const creds = { _store: {}, async store(key, val) { this._store[key] = val; }, async retrieve(key) { return this._store[key] || null; }, async delete(key) { delete this._store[key]; }, }; let result; jest.isolateModules(() => { const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager(creds, path.join(dir, 'config.json'), { info: () => {}, warn: () => {}, error: () => {} }); mgr.loadSecret(secretFile); const code = _mintCode(TEST_SECRET, 30, 1200); result = mgr.activate(code); }); const res = await result; expect(res.success).toBe(true); expect(res.activation.validationMethod).toBe('offline'); } finally { if (prevUrl === undefined) delete process.env.LICENSE_SERVER_URL; else process.env.LICENSE_SERVER_URL = prevUrl; global.fetch = originalFetch; _cleanup(dir); } }); test('rejects when server explicitly returns failure (no fallback)', async () => { const originalFetch = global.fetch; const dir = _tmpDir(); const prevUrl = process.env.LICENSE_SERVER_URL; process.env.LICENSE_SERVER_URL = 'https://license.test.example'; try { global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 403, json: async () => ({ error: 'Code revoked by administrator' }), }); const creds = { _store: {}, async store(key, val) { this._store[key] = val; }, async retrieve(key) { return this._store[key] || null; }, async delete(key) { delete this._store[key]; }, }; let result; jest.isolateModules(() => { const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager(creds, path.join(dir, 'config.json'), { info: () => {}, warn: () => {}, error: () => {} }); // Use a code with correct length (25 base32 chars = 5 groups of 5) result = mgr.activate('DC-ABCDE-FGHIJ-KLMNO-PQRST-UVWXY'); }); const res = await result; expect(res.success).toBe(false); expect(res.message).toMatch(/revoked/i); } finally { if (prevUrl === undefined) delete process.env.LICENSE_SERVER_URL; else process.env.LICENSE_SERVER_URL = prevUrl; global.fetch = originalFetch; _cleanup(dir); } }); }); // =========================================================================== // deactivate() // =========================================================================== describe('LicenseManager: deactivate()', () => { test('deactivates an active license', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 1300); await mgr.activate(code); expect(mgr.isPro()).toBe(true); const result = await mgr.deactivate(); expect(result.success).toBe(true); expect(result.message).toMatch(/deactivated/i); expect(mgr.activation).toBeNull(); expect(mgr.isPro()).toBe(false); } finally { await restore(); } }); test('clears credential store on deactivation', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 1400); await mgr.activate(code); expect(await mgr.credentialManager.retrieve('license.activation')).toBeTruthy(); await mgr.deactivate(); expect(await mgr.credentialManager.retrieve('license.activation')).toBeNull(); } finally { await restore(); } }); test('updates config.json to free tier after deactivation', async () => { const { mgr, restore, configFile } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 1500); await mgr.activate(code); // Verify config has license let config = JSON.parse(fs.readFileSync(configFile, 'utf8')); expect(config.license.active).toBe(true); await mgr.deactivate(); config = JSON.parse(fs.readFileSync(configFile, 'utf8')); expect(config.license.active).toBe(false); expect(config.license.tier).toBe('free'); expect(config.licenseBackup).toBeUndefined(); } finally { await restore(); } }); test('returns failure when no active license', async () => { const { mgr, restore } = _makeManager(); try { const result = await mgr.deactivate(); expect(result.success).toBe(false); expect(result.message).toMatch(/no active/i); } finally { await restore(); } }); }); // =========================================================================== // getStatus() // =========================================================================== describe('LicenseManager: getStatus()', () => { test('returns free tier when no activation', () => { const { mgr, restore } = _makeManager(); try { const status = mgr.getStatus(); expect(status.active).toBe(false); expect(status.tier).toBe('free'); expect(status.features).toEqual([]); expect(status.premiumFeatures).toBeTruthy(); } finally { restore(); } }); test('returns premium tier for active license', () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { mgr.activation = { code: 'DC-STATUS-TEST', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; const status = mgr.getStatus(); expect(status.active).toBe(true); expect(status.tier).toBe('premium'); expect(status.features).toContain('sso'); expect(status.daysRemaining).toBeGreaterThan(29); } finally { restore(); } }); test('returns free tier for expired license', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-EXPIRED-STATUS', durationDays: 30, lifetime: false, activatedAt: '2020-01-01T00:00:00Z', expiresAt: '2020-02-01T00:00:00Z', machineId: 'test', validationMethod: 'offline', features: ['sso'], }; const status = mgr.getStatus(); expect(status.active).toBe(false); expect(status.tier).toBe('free'); expect(status.expired).toBe(true); expect(status.features).toEqual([]); } finally { restore(); } }); test('returns null expiresAt/daysRemaining for lifetime', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-LIFETIME-STATUS', durationDays: 0, lifetime: true, activatedAt: new Date().toISOString(), expiresAt: new Date('2099-12-31T23:59:59.999Z').toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso', 'recipes'], }; const status = mgr.getStatus(); expect(status.active).toBe(true); expect(status.lifetime).toBe(true); expect(status.expiresAt).toBeNull(); expect(status.daysRemaining).toBeNull(); } finally { restore(); } }); test('masks the code in status output', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-ABCDE-FGHIJ-KLMNO-PQRST-UVWXY', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; const status = mgr.getStatus(); expect(status.code).not.toBe(mgr.activation.code); expect(status.code).toMatch(/^DC-/); expect(status.code).toContain('*****'); } finally { restore(); } }); }); // =========================================================================== // hasFeature() // =========================================================================== describe('LicenseManager: hasFeature()', () => { test('returns false when no activation', () => { const { mgr, restore } = _makeManager(); try { expect(mgr.hasFeature('sso')).toBe(false); } finally { restore(); } }); test('returns true for available feature on active license', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-FEATURE-TEST', durationDays: 90, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 90 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso', 'recipes'], }; expect(mgr.hasFeature('sso')).toBe(true); expect(mgr.hasFeature('recipes')).toBe(true); } finally { restore(); } }); test('returns false for unavailable feature', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-FEATURE-TEST', durationDays: 90, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 90 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; expect(mgr.hasFeature('swarm')).toBe(false); } finally { restore(); } }); test('returns false when license expired', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-EXPIRED-FEATURE', durationDays: 30, lifetime: false, activatedAt: '2020-01-01T00:00:00Z', expiresAt: '2020-02-01T00:00:00Z', machineId: 'test', validationMethod: 'offline', features: ['sso'], }; expect(mgr.hasFeature('sso')).toBe(false); } finally { restore(); } }); test('falls back to PREMIUM_FEATURES keys when activation.features missing', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-NO-FEATURES-LIST', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', // features omitted }; // Should default to all PREMIUM_FEATURES expect(mgr.hasFeature('sso')).toBe(true); expect(mgr.hasFeature('recipes')).toBe(true); expect(mgr.hasFeature('swarm')).toBe(true); } finally { restore(); } }); }); // =========================================================================== // isPro() / isExpired() / daysRemaining() // =========================================================================== describe('LicenseManager: isPro()', () => { test('false when no activation', () => { const { mgr, restore } = _makeManager(); try { expect(mgr.isPro()).toBe(false); } finally { restore(); } }); test('true for active non-lifetime license', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-PRO-TEST', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; expect(mgr.isPro()).toBe(true); } finally { restore(); } }); test('true for active lifetime license', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-LIFETIME-PRO', durationDays: 0, lifetime: true, activatedAt: new Date().toISOString(), expiresAt: new Date('2099-12-31').toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; expect(mgr.isPro()).toBe(true); } finally { restore(); } }); test('false for expired license', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-EXPIRED-PRO', durationDays: 30, lifetime: false, activatedAt: '2020-01-01T00:00:00Z', expiresAt: '2020-02-01T00:00:00Z', machineId: 'test', validationMethod: 'offline', features: ['sso'], }; expect(mgr.isPro()).toBe(false); } finally { restore(); } }); }); describe('LicenseManager: isExpired()', () => { test('true when no activation', () => { const { mgr, restore } = _makeManager(); try { expect(mgr.isExpired()).toBe(true); } finally { restore(); } }); test('false for lifetime (durationDays=0)', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { durationDays: 0, lifetime: true, expiresAt: null }; expect(mgr.isExpired()).toBe(false); } finally { restore(); } }); test('false for lifetime flag only (no durationDays)', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { lifetime: true, expiresAt: '2020-01-01T00:00:00Z' }; expect(mgr.isExpired()).toBe(false); } finally { restore(); } }); test('false when expiresAt is null/missing (treated as lifetime)', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { durationDays: 30, lifetime: false, expiresAt: null }; expect(mgr.isExpired()).toBe(false); } finally { restore(); } }); test('true when expiry is in the past', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { durationDays: 30, lifetime: false, expiresAt: new Date(Date.now() - 86400000).toISOString(), }; expect(mgr.isExpired()).toBe(true); } finally { restore(); } }); test('false when expiry is in the future', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { durationDays: 30, lifetime: false, expiresAt: new Date(Date.now() + 86400000).toISOString(), }; expect(mgr.isExpired()).toBe(false); } finally { restore(); } }); }); describe('LicenseManager: daysRemaining()', () => { test('returns 0 when no activation', () => { const { mgr, restore } = _makeManager(); try { expect(mgr.daysRemaining()).toBe(0); } finally { restore(); } }); test('returns positive days for active license', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { expiresAt: new Date(Date.now() + 15 * 86400000).toISOString(), }; const days = mgr.daysRemaining(); expect(days).toBeGreaterThanOrEqual(14); expect(days).toBeLessThanOrEqual(15); } finally { restore(); } }); test('returns negative for expired license (Math.ceil of negative)', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { expiresAt: new Date(Date.now() - 5 * 86400000).toISOString(), }; const days = mgr.daysRemaining(); expect(days).toBeLessThanOrEqual(-4); } finally { restore(); } }); }); // =========================================================================== // getMachineFingerprint() // =========================================================================== describe('LicenseManager: getMachineFingerprint()', () => { test('returns a 16-char hex string', () => { const { mgr, restore } = _makeManager(); try { const fp = mgr.getMachineFingerprint(); expect(fp).toMatch(/^[0-9a-f]{16}$/); } finally { restore(); } }); test('is stable across calls (same machine)', () => { const { mgr, restore } = _makeManager(); try { const fp1 = mgr.getMachineFingerprint(); const fp2 = mgr.getMachineFingerprint(); expect(fp1).toBe(fp2); } finally { restore(); } }); }); // =========================================================================== // requirePremium() middleware // =========================================================================== describe('LicenseManager: requirePremium() middleware', () => { test('calls next() when feature is available', () => { const { mgr, restore } = _makeManager(); try { mgr.activation = { code: 'DC-MW-TEST', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; const middleware = mgr.requirePremium('sso'); const req = {}; const res = {}; let nextCalled = false; middleware(req, res, (err) => { nextCalled = !err; }); expect(nextCalled).toBe(true); } finally { restore(); } }); test('returns 403 with premiumRequired when feature unavailable', () => { const { mgr, restore } = _makeManager(); try { const middleware = mgr.requirePremium('sso'); const req = {}; let status = null; let body = null; const res = { status(s) { status = s; return this; }, json(b) { body = b; return this; }, }; middleware(req, res, () => {}); expect(status).toBe(403); expect(body.success).toBe(false); expect(body.premiumRequired).toBe(true); expect(body.feature).toBe('sso'); } finally { restore(); } }); test('includes upgrade URL in 403 response', () => { const { mgr, restore } = _makeManager(); try { const middleware = mgr.requirePremium('recipes'); const req = {}; let body = null; const res = { status() { return this; }, json(b) { body = b; return this; }, }; middleware(req, res, () => {}); expect(body.upgradeUrl).toMatch(/settings.*license|license.*settings/i); } finally { restore(); } }); test('handles unknown feature key gracefully', () => { const { mgr, restore } = _makeManager(); try { const middleware = mgr.requirePremium('nonexistent'); const req = {}; let body = null; const res = { status() { return this; }, json(b) { body = b; return this; }, }; middleware(req, res, () => {}); expect(body.featureName).toBe('nonexistent'); } finally { restore(); } }); }); // =========================================================================== // loadSecret() // =========================================================================== describe('LicenseManager: loadSecret()', () => { test('loads secret from file and returns true', () => { const dir = _tmpDir(); try { const secretFile = path.join(dir, '.license-secret'); fs.writeFileSync(secretFile, 'testsecret123', 'utf8'); delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager({}, '/nonexistent', { info: () => {} }); const result = mgr.loadSecret(secretFile); expect(result).toBe(true); expect(mgr.masterSecretHash).toBe('testsecret123'); } finally { _cleanup(dir); } }); test('returns false when secret file does not exist', () => { const { mgr, restore } = _makeManager(); try { const result = mgr.loadSecret('/nonexistent/secret/path'); expect(result).toBe(false); expect(mgr.masterSecretHash).toBeNull(); } finally { restore(); } }); test('returns false and logs warning when secret file read throws', () => { const dir = _tmpDir(); // Mock fs.existsSync + fs.readFileSync; restore in finally so any // failure doesn't poison the rest of the suite. const origExists = fs.existsSync; const origRead = fs.readFileSync; let warnCalled = false; try { fs.existsSync = () => true; fs.readFileSync = () => { throw new Error('EACCES: permission denied'); }; delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); const mgr = new LicenseManager({}, '/nonexistent', { info: () => {}, warn: () => { warnCalled = true; } }); const result = mgr.loadSecret('/fake/secret/path'); expect(result).toBe(false); expect(mgr.masterSecretHash).toBeNull(); expect(warnCalled).toBe(true); } finally { fs.existsSync = origExists; fs.readFileSync = origRead; _cleanup(dir); } }); }); // =========================================================================== // _validateOffline() // =========================================================================== describe('LicenseManager: _validateOffline()', () => { test('validates real code with loaded secret', () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const code = _mintCode(TEST_SECRET, 30, 1600); const result = mgr._validateOffline(code); expect(result.valid).toBe(true); expect(result.durationDays).toBe(30); expect(result.codeId).toBe(1600); } finally { restore(); } }); test('rejects forged code (HMAC mismatch)', () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const wrongSecret = crypto.randomBytes(32).toString('hex'); const forgedCode = _mintCode(wrongSecret, 30, 1700); const result = mgr._validateOffline(forgedCode); expect(result.valid).toBe(false); expect(result.reason).toMatch(/signature|forged|corrupted/i); } finally { restore(); } }); test('returns validation-unavailable when no secret loaded', () => { const { mgr, restore } = _makeManager({ secret: null }); try { const code = _mintCode(TEST_SECRET, 30, 1800); const result = mgr._validateOffline(code); expect(result.valid).toBe(false); expect(result.reason).toMatch(/unavailable|internet/i); } finally { restore(); } }); test('returns invalid for malformed code string', () => { const { mgr, restore } = _makeManager({ secret: null }); try { const result = mgr._validateOffline('DC-GARBAGE'); expect(result.valid).toBe(false); } finally { restore(); } }); test('rejects code with unsupported version (forged version-2 payload)', () => { // Construct a code whose version nibble is 2 (not the current VERSION=1). // We can't use generateCode() because it hardcodes VERSION=1, so we // manually pack a version=2 payload, sign it with the correct HMAC, and // base32-encode it into the DC-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX format. const { generateCode, parseCode } = require('../license-keygen'); // Generate a valid version-1 code, decode it, bump the version nibble, // re-sign, re-encode. This gives a cryptographically well-formed code // with version=2 — the structural check should catch it. const realCode = generateCode(TEST_SECRET, 30, 1900); // Decode payload + signature from the real code const cleaned = realCode.replace(/^DC-/, '').replace(/-/g, ''); const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; function b32Decode(str) { let bits = ''; for (const ch of str.toUpperCase()) { bits += BASE32.indexOf(ch).toString(2).padStart(5, '0'); } const bytes = []; for (let i = 0; i + 8 <= bits.length; i += 8) bytes.push(parseInt(bits.substring(i, i + 8), 2)); return Buffer.from(bytes); } const decoded = b32Decode(cleaned); const payload = Buffer.from(decoded.subarray(0, 10)); // Overwrite version nibble: bits 12-15 of the first 16-bit value. const versionAndDuration = payload.readUInt16BE(0); const duration = versionAndDuration & 0x0FFF; payload.writeUInt16BE((2 << 12) | duration, 0); // version=2 // Re-sign with the same secret so HMAC is valid for the tampered payload const hmac = crypto.createHmac('sha256', TEST_SECRET).update(payload).digest(); const signature = hmac.subarray(0, 5); const combined = Buffer.concat([payload, signature]); // Re-encode base32 function b32Encode(buf) { let bits = ''; for (const b of buf) bits += b.toString(2).padStart(8, '0'); while (bits.length % 5 !== 0) bits += '0'; let result = ''; for (let i = 0; i < bits.length; i += 5) result += BASE32[parseInt(bits.substring(i, i + 5), 2)]; return result; } let encoded = b32Encode(combined); while (encoded.length < 25) encoded += '0'; encoded = encoded.substring(0, 25); const groups = []; for (let i = 0; i < 25; i += 5) groups.push(encoded.substring(i, i + 5)); const forgedV2Code = `DC-${groups.join('-')}`; // Verify the forged code actually parses as version=2 const parsed = parseCode(forgedV2Code); expect(parsed.version).toBe(2); const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { const result = mgr._validateOffline(forgedV2Code); expect(result.valid).toBe(false); expect(result.reason).toMatch(/version/i); } finally { restore(); } }); }); // =========================================================================== // _maskCode() // =========================================================================== describe('LicenseManager: _maskCode()', () => { test('masks a standard code (DC + 5 groups)', () => { const { mgr, restore } = _makeManager(); try { const masked = mgr._maskCode('DC-ABCDE-FGHIJ-KLMNO-PQRST-UVWXY'); expect(masked).toBe('DC-ABCDE-*****-*****-UVWXY'); } finally { restore(); } }); test('returns DC-***** for short code (< 4 groups)', () => { const { mgr, restore } = _makeManager(); try { expect(mgr._maskCode('DC-ABC')).toBe('DC-*****'); expect(mgr._maskCode('DC-ABC-DEF')).toBe('DC-*****'); } finally { restore(); } }); test('returns "none" for null/empty', () => { const { mgr, restore } = _makeManager(); try { expect(mgr._maskCode(null)).toBe('none'); expect(mgr._maskCode('')).toBe('none'); expect(mgr._maskCode(undefined)).toBe('none'); } finally { restore(); } }); }); // =========================================================================== // allowsLifetimeLicense() // =========================================================================== describe('LicenseManager: allowsLifetimeLicense()', () => { test('defaults to false', () => { const { mgr, restore } = _makeManager(); try { expect(mgr.allowsLifetimeLicense()).toBe(false); } finally { restore(); } }); test('returns true when ALLOW_LIFETIME_LICENSE=true', () => { const { mgr, restore } = _makeManager({ env: { ALLOW_LIFETIME_LICENSE: 'true' }, }); try { expect(mgr.allowsLifetimeLicense()).toBe(true); } finally { restore(); } }); test('returns false for non-"true" values', () => { const { mgr, restore } = _makeManager({ env: { ALLOW_LIFETIME_LICENSE: 'false' }, }); try { expect(mgr.allowsLifetimeLicense()).toBe(false); } finally { restore(); } }); }); // =========================================================================== // _updateConfig() — internal but critical for persistence // =========================================================================== describe('LicenseManager: _updateConfig()', () => { test('creates config.json if it does not exist', async () => { const { mgr, restore, configFile } = _makeManager(); try { expect(fs.existsSync(configFile)).toBe(false); mgr.activation = { code: 'DC-CREATE-CONFIG', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; await mgr._updateConfig(); expect(fs.existsSync(configFile)).toBe(true); const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); expect(config.license.active).toBe(true); expect(config.licenseBackup.code).toBe('DC-CREATE-CONFIG'); } finally { restore(); } }); test('preserves existing config fields when updating', async () => { const { mgr, restore, configFile } = _makeManager(); try { fs.writeFileSync(configFile, JSON.stringify({ tld: '.sami', existingField: 'preserved', }, null, 2)); mgr.activation = { code: 'DC-PRESERVE-CONFIG', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', validationMethod: 'offline', features: ['sso'], }; await mgr._updateConfig(); const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); expect(config.tld).toBe('.sami'); expect(config.existingField).toBe('preserved'); expect(config.license.active).toBe(true); } finally { restore(); } }); test('clears licenseBackup when activation is null/expired', async () => { const { mgr, restore, configFile } = _makeManager(); try { fs.writeFileSync(configFile, JSON.stringify({ license: { active: true, tier: 'premium' }, licenseBackup: { code: 'DC-OLD', durationDays: 30 }, }, null, 2)); mgr.activation = null; await mgr._updateConfig(); const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); expect(config.license.active).toBe(false); expect(config.license.tier).toBe('free'); expect(config.licenseBackup).toBeUndefined(); } finally { restore(); } }); test('does not crash when config file directory does not exist', async () => { const { mgr, restore } = _makeManager(); try { // Point to a path inside a nonexistent directory mgr.configFile = '/nonexistent/dir/config.json'; mgr.activation = { code: 'DC-NO-CRASH', durationDays: 30, lifetime: false, activatedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(), machineId: 'test', features: ['sso'], }; // Should not throw — _updateConfig catches internally await expect(mgr._updateConfig()).resolves.not.toThrow(); } finally { restore(); } }); }); // =========================================================================== // Full lifecycle integration // =========================================================================== describe('LicenseManager: full lifecycle integration', () => { test('activate → getStatus → deactivate → getStatus', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { // Initially free expect(mgr.getStatus().tier).toBe('free'); // Activate const code = _mintCode(TEST_SECRET, 90, 2000); const activateResult = await mgr.activate(code); expect(activateResult.success).toBe(true); // Check status is premium const activeStatus = mgr.getStatus(); expect(activeStatus.active).toBe(true); expect(activeStatus.tier).toBe('premium'); expect(activeStatus.durationDays).toBe(90); // Deactivate const deactivateResult = await mgr.deactivate(); expect(deactivateResult.success).toBe(true); // Back to free expect(mgr.getStatus().tier).toBe('free'); expect(mgr.isPro()).toBe(false); } finally { await restore(); } }); test('load() after activate() restores the same activation', async () => { const creds = { _store: {}, async store(key, val) { this._store[key] = val; }, async retrieve(key) { return this._store[key] || null; }, async delete(key) { delete this._store[key]; }, }; const dir = _tmpDir(); try { const configFile = path.join(dir, 'config.json'); const secretFile = path.join(dir, '.license-secret'); fs.writeFileSync(secretFile, TEST_SECRET, 'utf8'); delete require.cache[require.resolve('../src/managers/license-manager')]; const { LicenseManager } = require('../src/managers/license-manager'); // Activate const mgr1 = new LicenseManager(creds, configFile, { info: () => {} }); mgr1.loadSecret(secretFile); const code = _mintCode(TEST_SECRET, 30, 2100); await mgr1.activate(code); const originalCodeId = mgr1.activation.codeId; // Simulate restart: create a NEW manager with same creds + config const mgr2 = new LicenseManager(creds, configFile, { info: () => {} }); await mgr2.load(); expect(mgr2.activation).toBeTruthy(); expect(mgr2.activation.code).toBe(code); expect(mgr2.activation.codeId).toBe(originalCodeId); expect(mgr2.isPro()).toBe(true); } finally { _cleanup(dir); } }); test('freshly minted code validates as active (not expired)', async () => { const { mgr, restore } = _makeManager({ secret: TEST_SECRET }); try { // Generate a code and verify it independently const code = _mintCode(TEST_SECRET, 30, 2200); const verifyResult = verifyCode(TEST_SECRET, code); expect(verifyResult.valid).toBe(true); expect(verifyResult.durationDays).toBe(30); expect(verifyResult.expired).toBe(false); // Activate should succeed (code is cryptographically valid + not expired) const result = await mgr.activate(code); expect(result.success).toBe(true); expect(result.activation.expired).toBe(false); } finally { await restore(); } }); });