/** * 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); }); });