523 lines
24 KiB
JavaScript
523 lines
24 KiB
JavaScript
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);
|
|
});
|
|
});
|