Files
dashcaddy/dashcaddy-api/__tests__/network-registry.test.js

167 lines
6.3 KiB
JavaScript

/**
* Tests for src/network/registry.js (DC-140).
*
* Hermetic: no network, no filesystem. Providers are stub factories whose
* instances are plain objects implementing the documented contract surface
* (see src/network/provider.js).
*/
'use strict';
const { registry } = require('../src/network');
const { ProviderNotActiveError, ProviderError } = require('../src/network/errors');
const { CONTRACT_METHODS } = require('../src/network/provider');
/** Build a minimal contract-satisfying stub provider. */
function stubProvider(id, caps = {}) {
const p = {};
for (const m of CONTRACT_METHODS) {
p[m] = m === 'id' ? () => id : m === 'capabilities' ? () => ({
invites: false,
splitDnsApi: false,
perPeerFirewall: false,
selfHosted: false,
...caps,
}) : async () => ({});
}
return p;
}
beforeEach(() => {
registry.reset();
});
describe('network registry: inactive default', () => {
test('defaults to inactive — getActiveProvider() throws ProviderNotActiveError', () => {
expect(() => registry.getActiveProvider()).toThrow(ProviderNotActiveError);
});
test('getCapabilities() throws ProviderNotActiveError when unset', () => {
expect(() => registry.getCapabilities()).toThrow(ProviderNotActiveError);
});
test('active id is null before activation', () => {
expect(registry.getActiveProviderId()).toBeNull();
});
});
describe('network registry: registration', () => {
test('registerProvider stores a factory under its id', () => {
registry.registerProvider('stub-a', () => stubProvider('stub-a'));
expect(registry.listProviderIds()).toContain('stub-a');
});
test('registerProvider rejects a non-function factory', () => {
expect(() => registry.registerProvider('bad', 'not-a-function')).toThrow(TypeError);
});
test('registerProvider rejects an empty id', () => {
expect(() => registry.registerProvider('', () => stubProvider('x'))).toThrow(TypeError);
});
test('registerProvider re-registers over an existing id', () => {
registry.registerProvider('stub-a', () => stubProvider('stub-a'));
registry.registerProvider('stub-a', () => stubProvider('stub-a'));
expect(registry.listProviderIds().filter((i) => i === 'stub-a')).toHaveLength(1);
});
});
describe('network registry: activation + resolution', () => {
test('setActiveProvider(id) resolves the instance via the factory', () => {
const sentinel = stubProvider('stub-a');
registry.registerProvider('stub-a', () => sentinel);
const active = registry.setActiveProvider('stub-a');
expect(active).toBe(sentinel);
expect(registry.getActiveProvider()).toBe(sentinel);
expect(registry.getActiveProviderId()).toBe('stub-a');
});
test('setActiveProvider throws for an unregistered id', () => {
expect(() => registry.setActiveProvider('does-not-exist')).toThrow(/Unknown network provider/);
});
test('setActiveProvider rebuilds the instance when the factory is replaced', () => {
const first = stubProvider('stub-a');
const second = stubProvider('stub-a');
registry.registerProvider('stub-a', () => first);
registry.setActiveProvider('stub-a');
registry.registerProvider('stub-a', () => second);
expect(registry.getActiveProvider()).toBe(second);
});
test('setActiveProvider rejects an instance missing contract methods', () => {
registry.registerProvider('incomplete', () => ({ id: () => 'incomplete' }));
expect(() => registry.setActiveProvider('incomplete')).toThrow(/contract/i);
});
test('setActiveProvider rejects an instance whose id() mismatches', () => {
registry.registerProvider('stub-a', () => stubProvider('stub-b'));
expect(() => registry.setActiveProvider('stub-a')).toThrow(/different id/i);
});
test('reset() returns the registry to inactive', () => {
registry.registerProvider('stub-a', () => stubProvider('stub-a'));
registry.setActiveProvider('stub-a');
registry.reset();
expect(() => registry.getActiveProvider()).toThrow(ProviderNotActiveError);
expect(registry.getActiveProviderId()).toBeNull();
});
});
describe('network registry: capability flags', () => {
test('getCapabilities() returns the active provider capabilities', () => {
registry.registerProvider(
'stub-ts',
() => stubProvider('stub-ts', { invites: true, splitDnsApi: true })
);
registry.setActiveProvider('stub-ts');
expect(registry.getCapabilities()).toEqual({
invites: true,
splitDnsApi: true,
perPeerFirewall: false,
selfHosted: false,
});
});
});
describe('network errors', () => {
test('ProviderNotActiveError is a ProviderError with code provider_not_active', () => {
const e = new ProviderNotActiveError();
expect(e).toBeInstanceOf(ProviderError);
expect(e.code).toBe('provider_not_active');
expect(e.retryable).toBe(false);
});
test('ProviderError derives retryable from code when not given', () => {
expect(new ProviderError('x', { code: 'rate_limited' }).retryable).toBe(true);
expect(new ProviderError('x', { code: 'server_error' }).retryable).toBe(true);
expect(new ProviderError('x', { code: 'network_error' }).retryable).toBe(true);
expect(new ProviderError('x', { code: 'unauthorized' }).retryable).toBe(false);
expect(new ProviderError('x', { code: 'bad_input' }).retryable).toBe(false);
});
test('ProviderError honors an explicit retryable override', () => {
expect(new ProviderError('x', { code: 'rate_limited', retryable: false }).retryable).toBe(false);
expect(new ProviderError('x', { code: 'unknown', retryable: true }).retryable).toBe(true);
});
});
describe('network index: built-in tailscale registration', () => {
test('tailscale is registered and activatable via the package entry point', () => {
// The package entry registers the real tailscale factory; activation
// builds a real provider instance (lazy deps, no network at build time).
expect(registry.listProviderIds()).toContain('tailscale');
const provider = registry.setActiveProvider('tailscale');
expect(provider.id()).toBe('tailscale');
expect(provider.capabilities()).toEqual({
invites: true,
splitDnsApi: true,
perPeerFirewall: false,
selfHosted: false,
});
// And it deactivates cleanly again.
registry.reset();
expect(() => registry.getActiveProvider()).toThrow(ProviderNotActiveError);
});
});