377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
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: '<msg-1@example.com>',
|
|
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();
|