/** * DC-165: site/product uniformity bundle — tests. * 1. fleet is a gated premium feature (403 free / pass premium, and the * gate is mounted at ROUTER level so new sub-routes inherit it). * 2. offline activations minted before 'fleet' existed are forward- * migrated on load (creator lifetime key case). * 3. GET /api/v1/capabilities returns the running build's ground truth * derived from source modules (no drift by construction). */ 'use strict'; const express = require('express'); function buildGatedRouter(licenseLike) { const { LicenseManager } = require('../src/managers/license-manager'); // Real requirePremium() against a duck-typed instance: own properties // (hasFeature/isExpired/log) win over prototype defaults. const lm = Object.assign(Object.create(LicenseManager.prototype), { log: { warn() {} }, isExpired: () => false, }, licenseLike); const router = express.Router(); router.use(lm.requirePremium('fleet')); router.get('/fleet/hosts', (_req, res) => res.json({ ok: true })); return router; } describe('DC-165 fleet premium gate', () => { test('PREMIUM_FEATURES includes fleet (site entitlement contract)', () => { const { PREMIUM_FEATURES } = require('../src/managers/license-manager'); expect(PREMIUM_FEATURES.fleet).toBeDefined(); expect(PREMIUM_FEATURES.fleet.name).toMatch(/Fleet/i); }); test('free license: GET /fleet/hosts -> 403 premiumRequired fleet', async () => { const app = express(); app.use('/api/v1', buildGatedRouter({ hasFeature: () => false, isExpired: () => false, log: { warn() {} }, })); const srv = await new Promise((r) => { const s = app.listen(0, () => r(s)); }); try { const res = await fetch(`http://127.0.0.1:${srv.address().port}/api/v1/fleet/hosts`); expect(res.status).toBe(403); const body = await res.json(); expect(body.premiumRequired).toBe(true); expect(body.feature).toBe('fleet'); } finally { srv.close(); } }); test('premium license with fleet: request passes the gate', async () => { const app = express(); app.use('/api/v1', buildGatedRouter({ hasFeature: (f) => f === 'fleet', isExpired: () => false, log: { warn() {} }, })); const srv = await new Promise((r) => { const s = app.listen(0, () => r(s)); }); try { const res = await fetch(`http://127.0.0.1:${srv.address().port}/api/v1/fleet/hosts`); expect(res.status).toBe(200); } finally { srv.close(); } }); test('hasFeature fallback covers fleet for activations without explicit features list', () => { const { LicenseManager, PREMIUM_FEATURES } = require('../src/managers/license-manager'); const lm = Object.create(LicenseManager.prototype); lm.activation = { code: 'DC-X', expiresAt: '2099-01-01T00:00:00Z', features: undefined }; lm.isExpired = () => false; expect(lm.hasFeature('fleet')).toBe(true); expect(Object.keys(PREMIUM_FEATURES)).toContain('fleet'); }); }); describe('DC-165 offline activation forward-migration', () => { test('load() adds missing premium features to an offline activation and persists', async () => { const { LicenseManager, PREMIUM_FEATURES } = require('../src/managers/license-manager'); const stored = { code: 'DC-OLD-OLD-OLD-OLD-OLD', codeId: 1, durationDays: 0, lifetime: true, activatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2099-12-31T23:59:59.999Z', machineId: 'deadbeefdeadbeef', validationMethod: 'offline', features: ['sso', 'recipes', 'swarm'], // pre-fleet snapshot }; let persisted = null; const lm = new LicenseManager( { retrieve: async () => JSON.stringify(stored), store: async (key, value) => { persisted = { key, value }; }, delete: async () => {}, }, '/tmp/dc165-nonexistent-config.json', // no config fallback path { info() {}, warn() {}, error() {} } ); await lm.load(); expect(lm.activation.features).toEqual(Object.keys(PREMIUM_FEATURES)); expect(persisted).not.toBeNull(); expect(persisted.key).toBe('license.activation'); expect(JSON.parse(persisted.value).features).toContain('fleet'); }); test('migration is APPEND-ONLY: unknown legacy entries are preserved (judge P0)', async () => { const { LicenseManager, PREMIUM_FEATURES } = require('../src/managers/license-manager'); const stored = { code: 'DC-LEG-LEG-LEG-LEG-LEG', codeId: 3, durationDays: 0, lifetime: true, activatedAt: '2026-01-01T00:00:00.000Z', expiresAt: '2099-12-31T23:59:59.999Z', machineId: 'cafe0000deadbeef', validationMethod: 'offline', features: ['sso', 'recipes', 'swarm', 'legacy-beta-widget'], // extra rides along }; let persisted = null; const lm = new LicenseManager( { retrieve: async () => JSON.stringify(stored), store: async (key, value) => { persisted = { key, value }; }, delete: async () => {} }, '/tmp/dc165-nonexistent-config.json', { info() {}, warn() {}, error() {} } ); await lm.load(); expect(lm.activation.features).toEqual(Object.keys(PREMIUM_FEATURES).concat(['legacy-beta-widget'])); expect(JSON.parse(persisted.value).features).toContain('legacy-beta-widget'); }); test('online activations are NOT migrated (server is authoritative)', async () => { const { LicenseManager } = require('../src/managers/license-manager'); const activatedAt = new Date(Date.now() - 24 * 3600e3).toISOString(); // yesterday const stored = { code: 'DC-ONLINE', codeId: 2, durationDays: 365, activatedAt, expiresAt: new Date(Date.now() + 340 * 864e5).toISOString(), // within activatedAt+366d bound lastOnlineValidatedAt: activatedAt, machineId: 'aabbccddeeff0011', validationMethod: 'online', features: ['sso', 'recipes', 'swarm'], serverToken: 'tok', }; let storedCount = 0; const lm = new LicenseManager( { retrieve: async () => JSON.stringify(stored), store: async () => { storedCount++; }, delete: async () => {}, }, '/tmp/dc165-nonexistent-config.json', { info() {}, warn() {}, error() {} } ); // No LICENSE_SERVER_URL => _validateLoadedActivationForServerMode returns true immediately. await lm.load(); expect(lm.activation.features).toEqual(['sso', 'recipes', 'swarm']); // untouched expect(storedCount).toBe(0); // nothing persisted }); }); describe('DC-165 capabilities endpoint', () => { let srv; let base; beforeAll(async () => { const cap = require('../routes/capabilities'); const app = express(); app.use('/api/v1', cap.buildRouter()); srv = await new Promise((r) => { const s = app.listen(0, () => r(s)); }); base = `http://127.0.0.1:${srv.address().port}/api/v1`; }); afterAll(() => srv && srv.close()); test('returns source-derived ground truth with zero drift from modules', async () => { const res = await fetch(`${base}/capabilities`); expect(res.status).toBe(200); const j = await res.json(); const { APP_TEMPLATES } = require('../src/docker/app-templates'); const { TOOLS } = require('../src/mcp/mcp-core'); const { PREMIUM_FEATURES } = require('../src/managers/license-manager'); expect(j.success).toBe(true); expect(j.product).toBe('dashcaddy'); expect(j.templates.count).toBe(Object.keys(APP_TEMPLATES).length); expect(j.templates.count).toBeGreaterThanOrEqual(78); const { MCP_RUNTIME } = require('../routes/mcp'); expect(j.mcp.transport).toBe(MCP_RUNTIME.transport); expect(j.mcp.protocolVersion).toBe(MCP_RUNTIME.protocolVersion); expect(j.mcp.protocolVersions).toEqual(MCP_RUNTIME.supportedProtocolVersions); expect(j.mcp.tools).toEqual(TOOLS.map((t) => t.name).sort()); expect(j.premiumFeatures).toEqual(Object.keys(PREMIUM_FEATURES).sort()); expect(j.premiumFeatures).toContain('fleet'); expect(j.i18n.languageCount).toBe(31); expect(j.version).toMatch(/^\d+\.\d+\.\d+/); }); test('no secrets or host-specific data in the payload', async () => { const res = await fetch(`${base}/capabilities`); const text = await res.text(); expect(text).not.toMatch(/license\/(activation|secret)/i); expect(text).not.toMatch(/sami/i); expect(text).not.toMatch(/100\.|192\.168|dashcaddy\.net|status\.sami/i); expect(text).not.toMatch(/"token"|"api_?key"|"password"|"secret"/i); }); });