import { tap, expect } from '@git.zone/tstest/tapbundle'; import { DcRouterApiClient, Route, RouteBuilder, RouteManager, Certificate, CertificateManager, ApiToken, ApiTokenBuilder, ApiTokenManager, RemoteIngress, RemoteIngressBuilder, RemoteIngressManager, Email, EmailManager, StatsManager, ConfigManager, LogManager, RadiusManager, RadiusClientManager, RadiusVlanManager, RadiusSessionManager, } from '../ts_apiclient/index.js'; // ============================================================================= // Instantiation & Structure // ============================================================================= tap.test('DcRouterApiClient - should instantiate with baseUrl', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); expect(client).toBeTruthy(); expect(client.baseUrl).toEqual('https://localhost:3000'); expect(client.identity).toBeUndefined(); }); tap.test('DcRouterApiClient - should strip trailing slashes from baseUrl', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000///' }); expect(client.baseUrl).toEqual('https://localhost:3000'); }); tap.test('DcRouterApiClient - should accept optional apiToken', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000', apiToken: 'dcr_test_token', }); expect(client.apiToken).toEqual('dcr_test_token'); }); tap.test('DcRouterApiClient - should have all resource managers', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); expect(client.routes).toBeInstanceOf(RouteManager); expect(client.certificates).toBeInstanceOf(CertificateManager); expect(client.apiTokens).toBeInstanceOf(ApiTokenManager); expect(client.remoteIngress).toBeInstanceOf(RemoteIngressManager); expect(client.stats).toBeInstanceOf(StatsManager); expect(client.config).toBeInstanceOf(ConfigManager); expect(client.logs).toBeInstanceOf(LogManager); expect(client.emails).toBeInstanceOf(EmailManager); expect(client.radius).toBeInstanceOf(RadiusManager); }); // ============================================================================= // buildRequestPayload // ============================================================================= tap.test('DcRouterApiClient - buildRequestPayload includes identity when set', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const identity = { jwt: 'test-jwt', userId: 'user1', name: 'Admin', expiresAt: Date.now() + 3600000, }; client.identity = identity; const payload = client.buildRequestPayload({ extra: 'data' }); expect(payload.identity).toEqual(identity); expect(payload.extra).toEqual('data'); }); tap.test('DcRouterApiClient - buildRequestPayload includes apiToken when set', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000', apiToken: 'dcr_abc123', }); const payload = client.buildRequestPayload(); expect(payload.apiToken).toEqual('dcr_abc123'); }); tap.test('DcRouterApiClient - buildRequestPayload with both identity and apiToken', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000', apiToken: 'dcr_abc123', }); client.identity = { jwt: 'test-jwt', userId: 'user1', name: 'Admin', expiresAt: Date.now() + 3600000, }; const payload = client.buildRequestPayload({ foo: 'bar' }); expect(payload.identity).toBeTruthy(); expect(payload.apiToken).toEqual('dcr_abc123'); expect(payload.foo).toEqual('bar'); }); // ============================================================================= // Route Builder // ============================================================================= tap.test('RouteBuilder - should support fluent builder pattern', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const builder = client.routes.build(); expect(builder).toBeInstanceOf(RouteBuilder); // Fluent methods return `this` (same reference) const result = builder .setName('test-route') .setMatch({ ports: 443, domains: 'example.com' }) .setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] }) .setEnabled(true); expect(result === builder).toBeTrue(); }); // ============================================================================= // ApiToken Builder // ============================================================================= tap.test('ApiTokenBuilder - should support fluent builder pattern', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const builder = client.apiTokens.build(); expect(builder).toBeInstanceOf(ApiTokenBuilder); const result = builder .setName('ci-token') .setScopes(['routes:read', 'routes:write']) .addScope('config:read') .setExpiresInDays(30); expect(result === builder).toBeTrue(); }); // ============================================================================= // RemoteIngress Builder // ============================================================================= tap.test('RemoteIngressBuilder - should support fluent builder pattern', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const builder = client.remoteIngress.build(); expect(builder).toBeInstanceOf(RemoteIngressBuilder); const result = builder .setName('edge-1') .setListenPorts([80, 443]) .setAutoDerivePorts(true) .setTags(['production']); expect(result === builder).toBeTrue(); }); // ============================================================================= // Route resource class // ============================================================================= tap.test('Route - should hydrate from IMergedRoute data', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const route = new Route(client, { route: { name: 'test-route', match: { ports: 443, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }] }, }, source: 'programmatic', enabled: true, overridden: false, storedRouteId: 'route-123', createdAt: 1000, updatedAt: 2000, }); expect(route.name).toEqual('test-route'); expect(route.source).toEqual('programmatic'); expect(route.enabled).toEqual(true); expect(route.overridden).toEqual(false); expect(route.storedRouteId).toEqual('route-123'); expect(route.routeConfig.match.ports).toEqual(443); }); tap.test('Route - should throw on update/delete/toggle for hardcoded routes', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const route = new Route(client, { route: { name: 'hardcoded-route', match: { ports: 80 }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] }, }, source: 'hardcoded', enabled: true, overridden: false, // No storedRouteId for hardcoded routes }); let updateError: Error | undefined; try { await route.update({ name: 'new-name' }); } catch (e) { updateError = e as Error; } expect(updateError).toBeTruthy(); expect(updateError!.message).toInclude('hardcoded'); let deleteError: Error | undefined; try { await route.delete(); } catch (e) { deleteError = e as Error; } expect(deleteError).toBeTruthy(); let toggleError: Error | undefined; try { await route.toggle(false); } catch (e) { toggleError = e as Error; } expect(toggleError).toBeTruthy(); }); // ============================================================================= // Certificate resource class // ============================================================================= tap.test('Certificate - should hydrate from ICertificateInfo data', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const cert = new Certificate(client, { domain: 'example.com', routeNames: ['main-route'], status: 'valid', source: 'acme', tlsMode: 'terminate', expiryDate: '2027-01-01T00:00:00Z', issuer: "Let's Encrypt", canReprovision: true, }); expect(cert.domain).toEqual('example.com'); expect(cert.status).toEqual('valid'); expect(cert.source).toEqual('acme'); expect(cert.canReprovision).toEqual(true); expect(cert.routeNames.length).toEqual(1); }); // ============================================================================= // ApiToken resource class // ============================================================================= tap.test('ApiToken - should hydrate from IApiTokenInfo data', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const token = new ApiToken( client, { id: 'token-1', name: 'ci-token', scopes: ['routes:read', 'routes:write'], createdAt: Date.now(), expiresAt: null, lastUsedAt: null, enabled: true, }, 'dcr_secret_value', ); expect(token.id).toEqual('token-1'); expect(token.name).toEqual('ci-token'); expect(token.scopes.length).toEqual(2); expect(token.enabled).toEqual(true); expect(token.tokenValue).toEqual('dcr_secret_value'); }); // ============================================================================= // RemoteIngress resource class // ============================================================================= tap.test('RemoteIngress - should hydrate from IRemoteIngress data', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const edge = new RemoteIngress(client, { id: 'edge-1', name: 'test-edge', secret: 'secret123', listenPorts: [80, 443], enabled: true, autoDerivePorts: true, tags: ['prod'], createdAt: 1000, updatedAt: 2000, effectiveListenPorts: [80, 443, 8080], manualPorts: [80, 443], derivedPorts: [8080], }); expect(edge.id).toEqual('edge-1'); expect(edge.name).toEqual('test-edge'); expect(edge.listenPorts.length).toEqual(2); expect(edge.effectiveListenPorts!.length).toEqual(3); expect(edge.autoDerivePorts).toEqual(true); }); // ============================================================================= // Email resource class // ============================================================================= tap.test('Email - should hydrate from IEmail data', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); const email = new Email(client, { id: 'email-1', direction: 'inbound', status: 'delivered', from: 'sender@example.com', to: 'recipient@example.com', subject: 'Test email', timestamp: '2026-03-06T00:00:00Z', messageId: '', size: '1234', }); expect(email.id).toEqual('email-1'); expect(email.direction).toEqual('inbound'); expect(email.status).toEqual('delivered'); expect(email.from).toEqual('sender@example.com'); expect(email.subject).toEqual('Test email'); }); // ============================================================================= // RadiusManager structure // ============================================================================= tap.test('RadiusManager - should have sub-managers', async () => { const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' }); expect(client.radius.clients).toBeInstanceOf(RadiusClientManager); expect(client.radius.vlans).toBeInstanceOf(RadiusVlanManager); expect(client.radius.sessions).toBeInstanceOf(RadiusSessionManager); }); // ============================================================================= // Exports verification // ============================================================================= tap.test('Exports - all expected classes should be importable', async () => { expect(DcRouterApiClient).toBeTruthy(); expect(Route).toBeTruthy(); expect(RouteBuilder).toBeTruthy(); expect(RouteManager).toBeTruthy(); expect(Certificate).toBeTruthy(); expect(CertificateManager).toBeTruthy(); expect(ApiToken).toBeTruthy(); expect(ApiTokenBuilder).toBeTruthy(); expect(ApiTokenManager).toBeTruthy(); expect(RemoteIngress).toBeTruthy(); expect(RemoteIngressBuilder).toBeTruthy(); expect(RemoteIngressManager).toBeTruthy(); expect(Email).toBeTruthy(); expect(EmailManager).toBeTruthy(); expect(StatsManager).toBeTruthy(); expect(ConfigManager).toBeTruthy(); expect(LogManager).toBeTruthy(); expect(RadiusManager).toBeTruthy(); expect(RadiusClientManager).toBeTruthy(); expect(RadiusVlanManager).toBeTruthy(); expect(RadiusSessionManager).toBeTruthy(); }); export default tap.start();