167 lines
6.3 KiB
JavaScript
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);
|
|
});
|
|
});
|