383 lines
12 KiB
TypeScript
383 lines
12 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { WorkHosterHandler } from '../ts/opsserver/handlers/workhoster.handler.js';
|
|
import * as plugins from '../ts/plugins.js';
|
|
import * as interfaces from '../ts_interfaces/index.js';
|
|
|
|
type TScope = interfaces.data.TApiTokenScope;
|
|
|
|
const fireTypedRequest = async (
|
|
router: plugins.typedrequest.TypedRouter,
|
|
method: string,
|
|
request: Record<string, any>,
|
|
) => {
|
|
return await router.routeAndAddResponse({
|
|
method,
|
|
request,
|
|
response: {},
|
|
correlation: {
|
|
id: `${method}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
phase: 'request',
|
|
},
|
|
} as any, { localRequest: true, skipHooks: true }) as any;
|
|
};
|
|
|
|
const makeApiTokenManager = (scopes: TScope[]) => {
|
|
const token = {
|
|
id: 'token-1',
|
|
name: 'workhoster-test-token',
|
|
scopes,
|
|
createdBy: 'token-user',
|
|
createdAt: Date.now(),
|
|
expiresAt: null,
|
|
lastUsedAt: null,
|
|
enabled: true,
|
|
} as interfaces.data.IStoredApiToken;
|
|
|
|
return {
|
|
validateToken: async (rawToken: string) => rawToken === 'valid-token' ? token : null,
|
|
hasScope: (storedToken: interfaces.data.IStoredApiToken, scope: TScope) => storedToken.scopes.includes(scope),
|
|
};
|
|
};
|
|
|
|
const makeRouteConfigManager = () => {
|
|
const routes = new Map<string, interfaces.data.IRoute>();
|
|
let nextRouteNumber = 1;
|
|
|
|
return {
|
|
routes,
|
|
manager: {
|
|
findApiRouteByExternalKey: (externalKey: string) => {
|
|
return Array.from(routes.values()).find((route) =>
|
|
route.origin === 'api' && route.metadata?.externalKey === externalKey,
|
|
);
|
|
},
|
|
createRoute: async (
|
|
route: interfaces.data.IDcRouterRouteConfig,
|
|
createdBy: string,
|
|
enabled = true,
|
|
metadata?: interfaces.data.IRouteMetadata,
|
|
) => {
|
|
const id = `route-${nextRouteNumber++}`;
|
|
routes.set(id, {
|
|
id,
|
|
route,
|
|
enabled,
|
|
createdBy,
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
origin: 'api',
|
|
metadata,
|
|
});
|
|
return id;
|
|
},
|
|
updateRoute: async (
|
|
id: string,
|
|
patch: {
|
|
route?: Partial<interfaces.data.IDcRouterRouteConfig>;
|
|
enabled?: boolean;
|
|
metadata?: Partial<interfaces.data.IRouteMetadata>;
|
|
},
|
|
) => {
|
|
const storedRoute = routes.get(id);
|
|
if (!storedRoute) return { success: false, message: 'Route not found' };
|
|
if (patch.route) {
|
|
storedRoute.route = { ...storedRoute.route, ...patch.route } as interfaces.data.IDcRouterRouteConfig;
|
|
}
|
|
if (patch.enabled !== undefined) {
|
|
storedRoute.enabled = patch.enabled;
|
|
}
|
|
if (patch.metadata) {
|
|
storedRoute.metadata = { ...storedRoute.metadata, ...patch.metadata };
|
|
}
|
|
storedRoute.updatedAt = Date.now();
|
|
return { success: true };
|
|
},
|
|
deleteRoute: async (id: string) => {
|
|
const deleted = routes.delete(id);
|
|
return deleted ? { success: true } : { success: false, message: 'Route not found' };
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
const setupHandler = (options: {
|
|
scopes: TScope[];
|
|
dcRouterRef?: Record<string, any>;
|
|
}) => {
|
|
const typedrouter = new plugins.typedrequest.TypedRouter();
|
|
const opsServerRef: any = {
|
|
typedrouter,
|
|
adminHandler: {
|
|
adminIdentityGuard: {
|
|
exec: async () => false,
|
|
},
|
|
},
|
|
dcRouterRef: {
|
|
options: {},
|
|
apiTokenManager: makeApiTokenManager(options.scopes),
|
|
...options.dcRouterRef,
|
|
},
|
|
};
|
|
|
|
new WorkHosterHandler(opsServerRef);
|
|
return { typedrouter, opsServerRef };
|
|
};
|
|
|
|
tap.test('WorkHosterHandler exposes capabilities and managed domains with workhosters:read', async () => {
|
|
const { typedrouter } = setupHandler({
|
|
scopes: ['workhosters:read'],
|
|
dcRouterRef: {
|
|
options: {
|
|
remoteIngressConfig: { enabled: true },
|
|
dnsScopes: ['example.com'],
|
|
http3: { enabled: false },
|
|
},
|
|
routeConfigManager: {},
|
|
smartProxy: {},
|
|
emailDomainManager: {},
|
|
emailServer: {},
|
|
dnsManager: {
|
|
listDomains: async () => [
|
|
{ id: 'domain-1', name: 'example.com', source: 'dcrouter', authoritative: true },
|
|
{ id: 'domain-2', name: 'provider.example', source: 'provider', providerId: 'cloudflare-1', authoritative: false },
|
|
],
|
|
toPublicDomain: (domainDoc: any) => ({
|
|
...domainDoc,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
createdBy: 'test',
|
|
}),
|
|
},
|
|
},
|
|
});
|
|
|
|
const capabilitiesResult = await fireTypedRequest(typedrouter, 'getGatewayCapabilities', {
|
|
apiToken: 'valid-token',
|
|
});
|
|
expect(capabilitiesResult.error).toBeUndefined();
|
|
expect(capabilitiesResult.response.capabilities.routes.idempotentSync).toEqual(true);
|
|
expect(capabilitiesResult.response.capabilities.domains.read).toEqual(true);
|
|
expect(capabilitiesResult.response.capabilities.certificates.export).toEqual(true);
|
|
expect(capabilitiesResult.response.capabilities.email.inbound).toEqual(true);
|
|
expect(capabilitiesResult.response.capabilities.remoteIngress.enabled).toEqual(true);
|
|
expect(capabilitiesResult.response.capabilities.dns.authoritative).toEqual(true);
|
|
expect(capabilitiesResult.response.capabilities.http3.enabled).toEqual(false);
|
|
|
|
const domainsResult = await fireTypedRequest(typedrouter, 'getWorkHosterDomains', {
|
|
apiToken: 'valid-token',
|
|
});
|
|
expect(domainsResult.error).toBeUndefined();
|
|
expect(domainsResult.response.domains.length).toEqual(2);
|
|
expect(domainsResult.response.domains[0].capabilities.canCreateSubdomains).toEqual(true);
|
|
expect(domainsResult.response.domains[1].capabilities.canManageDnsRecords).toEqual(true);
|
|
expect(domainsResult.response.domains[1].capabilities.canIssueCertificates).toEqual(true);
|
|
expect(domainsResult.response.domains[1].capabilities.canHostEmail).toEqual(true);
|
|
});
|
|
|
|
tap.test('WorkHosterHandler syncs WorkApp routes idempotently with workhosters:write', async () => {
|
|
const routeConfig = makeRouteConfigManager();
|
|
const { typedrouter } = setupHandler({
|
|
scopes: ['workhosters:write'],
|
|
dcRouterRef: {
|
|
options: {},
|
|
routeConfigManager: routeConfig.manager,
|
|
},
|
|
});
|
|
const ownership: interfaces.data.IWorkAppRouteOwnership = {
|
|
workHosterType: 'onebox',
|
|
workHosterId: 'box-1',
|
|
workAppId: 'app-1',
|
|
hostname: 'app.example.com',
|
|
};
|
|
|
|
const createResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
|
apiToken: 'valid-token',
|
|
ownership,
|
|
route: {
|
|
match: { ports: [443], domains: ['app.example.com'] },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '10.0.0.2', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(createResult.error).toBeUndefined();
|
|
expect(createResult.response).toEqual({ success: true, action: 'created', routeId: 'route-1' });
|
|
expect(routeConfig.routes.size).toEqual(1);
|
|
|
|
const createdRoute = routeConfig.routes.get('route-1')!;
|
|
expect(createdRoute.createdBy).toEqual('token-user');
|
|
expect(createdRoute.route.name?.startsWith('workapp-onebox-box-1-app-1-app-example-com')).toEqual(true);
|
|
expect(createdRoute.metadata).toEqual({
|
|
ownerType: 'workhoster',
|
|
workHosterType: 'onebox',
|
|
workHosterId: 'box-1',
|
|
workAppId: 'app-1',
|
|
externalKey: 'onebox:box-1:app-1:app.example.com',
|
|
});
|
|
|
|
const updateResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
|
apiToken: 'valid-token',
|
|
ownership,
|
|
enabled: false,
|
|
route: {
|
|
name: 'updated-workapp-route',
|
|
match: { ports: [443], domains: ['app.example.com'] },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '10.0.0.3', port: 3000 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(updateResult.error).toBeUndefined();
|
|
expect(updateResult.response).toEqual({ success: true, action: 'updated', routeId: 'route-1' });
|
|
expect(routeConfig.routes.size).toEqual(1);
|
|
expect(routeConfig.routes.get('route-1')?.enabled).toEqual(false);
|
|
expect(routeConfig.routes.get('route-1')?.route.name).toEqual('updated-workapp-route');
|
|
expect(routeConfig.routes.get('route-1')?.route.action.targets?.[0].host).toEqual('10.0.0.3');
|
|
|
|
const deleteResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
|
apiToken: 'valid-token',
|
|
ownership,
|
|
delete: true,
|
|
});
|
|
|
|
expect(deleteResult.error).toBeUndefined();
|
|
expect(deleteResult.response).toEqual({ success: true, action: 'deleted', routeId: 'route-1' });
|
|
expect(routeConfig.routes.size).toEqual(0);
|
|
|
|
const unchangedResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
|
apiToken: 'valid-token',
|
|
ownership,
|
|
delete: true,
|
|
});
|
|
|
|
expect(unchangedResult.error).toBeUndefined();
|
|
expect(unchangedResult.response).toEqual({ success: true, action: 'unchanged' });
|
|
});
|
|
|
|
tap.test('WorkHosterHandler rejects WorkApp route sync without workhosters:write', async () => {
|
|
const routeConfig = makeRouteConfigManager();
|
|
const { typedrouter } = setupHandler({
|
|
scopes: ['workhosters:read'],
|
|
dcRouterRef: {
|
|
options: {},
|
|
routeConfigManager: routeConfig.manager,
|
|
},
|
|
});
|
|
|
|
const result = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
|
apiToken: 'valid-token',
|
|
ownership: {
|
|
workHosterType: 'onebox',
|
|
workHosterId: 'box-1',
|
|
workAppId: 'app-1',
|
|
hostname: 'app.example.com',
|
|
},
|
|
delete: true,
|
|
});
|
|
|
|
expect(result.error?.text).toEqual('insufficient scope');
|
|
expect(routeConfig.routes.size).toEqual(0);
|
|
});
|
|
|
|
tap.test('WorkHosterHandler exposes and syncs WorkApp mail identities', async () => {
|
|
const syncedRequests: Array<{ data: any; userId: string }> = [];
|
|
const identity: interfaces.data.IWorkAppMailIdentity = {
|
|
id: 'mail-1',
|
|
externalKey: 'onebox:box-1:app-1:hello@example.com',
|
|
ownership: {
|
|
workHosterType: 'onebox',
|
|
workHosterId: 'box-1',
|
|
workAppId: 'app-1',
|
|
},
|
|
address: 'hello@example.com',
|
|
localPart: 'hello',
|
|
domain: 'example.com',
|
|
enabled: true,
|
|
inbound: {
|
|
enabled: true,
|
|
targetHost: '10.0.0.2',
|
|
targetPort: 2525,
|
|
},
|
|
smtp: {
|
|
enabled: true,
|
|
username: 'workapp-user',
|
|
},
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
createdBy: 'token-user',
|
|
};
|
|
const { typedrouter } = setupHandler({
|
|
scopes: ['workhosters:read', 'workhosters:write'],
|
|
dcRouterRef: {
|
|
options: {},
|
|
workAppMailManager: {
|
|
listMailIdentities: async (filter: any) => filter.workAppId === 'app-1' ? [identity] : [],
|
|
syncMailIdentity: async (data: any, userId: string) => {
|
|
syncedRequests.push({ data, userId });
|
|
return {
|
|
success: true,
|
|
action: 'created',
|
|
identity,
|
|
smtpCredentials: {
|
|
username: 'workapp-user',
|
|
password: 'generated-password',
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const listResult = await fireTypedRequest(typedrouter, 'getWorkAppMailIdentities', {
|
|
apiToken: 'valid-token',
|
|
ownership: { workAppId: 'app-1' },
|
|
});
|
|
expect(listResult.error).toBeUndefined();
|
|
expect(listResult.response.identities).toEqual([identity]);
|
|
|
|
const syncResult = await fireTypedRequest(typedrouter, 'syncWorkAppMailIdentity', {
|
|
apiToken: 'valid-token',
|
|
ownership: identity.ownership,
|
|
localPart: 'hello',
|
|
domain: 'example.com',
|
|
inbound: identity.inbound,
|
|
});
|
|
expect(syncResult.error).toBeUndefined();
|
|
expect(syncResult.response.success).toEqual(true);
|
|
expect(syncResult.response.smtpCredentials.password).toEqual('generated-password');
|
|
expect(syncedRequests[0].userId).toEqual('token-user');
|
|
});
|
|
|
|
tap.test('WorkHosterHandler rejects WorkApp mail sync without workhosters:write', async () => {
|
|
const { typedrouter } = setupHandler({
|
|
scopes: ['workhosters:read'],
|
|
dcRouterRef: {
|
|
options: {},
|
|
workAppMailManager: {
|
|
syncMailIdentity: async () => ({ success: true }),
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await fireTypedRequest(typedrouter, 'syncWorkAppMailIdentity', {
|
|
apiToken: 'valid-token',
|
|
ownership: {
|
|
workHosterType: 'onebox',
|
|
workHosterId: 'box-1',
|
|
workAppId: 'app-1',
|
|
},
|
|
localPart: 'hello',
|
|
domain: 'example.com',
|
|
});
|
|
|
|
expect(result.error?.text).toEqual('insufficient scope');
|
|
});
|
|
|
|
export default tap.start();
|