201 lines
6.4 KiB
TypeScript
201 lines
6.4 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { CertificateHandler } from '../ts/opsserver/handlers/certificate.handler.js';
|
|
import { AcmeCertDoc, DcRouterDb } from '../ts/db/index.js';
|
|
import * as plugins from '../ts/plugins.js';
|
|
import * as interfaces from '../ts_interfaces/index.js';
|
|
|
|
type TScope = interfaces.data.TApiTokenScope;
|
|
|
|
const createTestDb = async () => {
|
|
const storagePath = plugins.path.join(
|
|
plugins.os.tmpdir(),
|
|
`dcrouter-cert-api-token-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
);
|
|
|
|
DcRouterDb.resetInstance();
|
|
const db = DcRouterDb.getInstance({
|
|
storagePath,
|
|
dbName: `dcrouter-test-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
});
|
|
await db.start();
|
|
await db.getDb().mongoDb.createCollection('__test_init');
|
|
|
|
return {
|
|
async cleanup() {
|
|
await db.stop();
|
|
DcRouterDb.resetInstance();
|
|
await plugins.fs.promises.rm(storagePath, { recursive: true, force: true });
|
|
},
|
|
};
|
|
};
|
|
|
|
const makeApiTokenManager = (scopes: TScope[]) => {
|
|
const token = {
|
|
id: 'token-1',
|
|
name: 'certificate-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 setupHandler = (scopes: TScope[], options?: {
|
|
routes?: any[];
|
|
certProvisionScheduler?: any;
|
|
certProvisionFunction?: (...args: any[]) => any;
|
|
}) => {
|
|
const typedrouter = new plugins.typedrequest.TypedRouter();
|
|
const opsServerRef: any = {
|
|
typedrouter,
|
|
adminHandler: {
|
|
adminIdentityGuard: {
|
|
exec: async () => false,
|
|
},
|
|
},
|
|
dcRouterRef: {
|
|
apiTokenManager: makeApiTokenManager(scopes),
|
|
certificateStatusMap: new Map(),
|
|
smartProxy: {
|
|
settings: options?.certProvisionFunction ? {
|
|
certProvisionFunction: options.certProvisionFunction,
|
|
} : {},
|
|
routeManager: { getRoutes: () => options?.routes ?? [] },
|
|
getCertificateStatus: async () => null,
|
|
},
|
|
certProvisionScheduler: options?.certProvisionScheduler ?? null,
|
|
},
|
|
};
|
|
|
|
new CertificateHandler(opsServerRef);
|
|
return { typedrouter, opsServerRef };
|
|
};
|
|
|
|
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 testDbPromise = createTestDb();
|
|
|
|
tap.test('CertificateHandler allows API-token export with certificates:read', async () => {
|
|
await testDbPromise;
|
|
|
|
const certDoc = new AcmeCertDoc();
|
|
certDoc.id = 'cert-1';
|
|
certDoc.domainName = 'example.com';
|
|
certDoc.created = 1;
|
|
certDoc.validUntil = 2;
|
|
certDoc.privateKey = '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----';
|
|
certDoc.publicKey = '-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----';
|
|
certDoc.csr = '';
|
|
await certDoc.save();
|
|
|
|
const { typedrouter } = setupHandler(['certificates:read']);
|
|
const result = await fireTypedRequest(typedrouter, 'exportCertificate', {
|
|
apiToken: 'valid-token',
|
|
domain: 'example.com',
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.response.success).toEqual(true);
|
|
expect(result.response.cert.domainName).toEqual('example.com');
|
|
expect(result.response.cert.privateKey).toContain('BEGIN PRIVATE KEY');
|
|
expect(result.response.cert.publicKey).toContain('BEGIN CERTIFICATE');
|
|
});
|
|
|
|
tap.test('CertificateHandler rejects API-token export without certificates:read', async () => {
|
|
const { typedrouter } = setupHandler(['certificates:write']);
|
|
const result = await fireTypedRequest(typedrouter, 'exportCertificate', {
|
|
apiToken: 'valid-token',
|
|
domain: 'example.com',
|
|
});
|
|
|
|
expect(result.error?.text).toEqual('insufficient scope');
|
|
});
|
|
|
|
tap.test('CertificateHandler allows API-token import with certificates:write', async () => {
|
|
await testDbPromise;
|
|
|
|
const { typedrouter, opsServerRef } = setupHandler(['certificates:write']);
|
|
const result = await fireTypedRequest(typedrouter, 'importCertificate', {
|
|
apiToken: 'valid-token',
|
|
cert: {
|
|
id: 'cert-2',
|
|
domainName: 'imported.example.com',
|
|
created: 3,
|
|
validUntil: 4,
|
|
privateKey: '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----',
|
|
publicKey: '-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----',
|
|
csr: '',
|
|
},
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.response.success).toEqual(true);
|
|
expect((await AcmeCertDoc.findByDomain('imported.example.com'))?.id).toEqual('cert-2');
|
|
expect(opsServerRef.dcRouterRef.certificateStatusMap.get('imported.example.com')?.status).toEqual('valid');
|
|
});
|
|
|
|
tap.test('CertificateHandler reports active certificate backoff as failed with root cause', async () => {
|
|
await testDbPromise;
|
|
|
|
const lastError = 'DNS-01 failed for stack.gallery: DnsManager: no managed domain found for _acme-challenge.stack.gallery.';
|
|
const retryAfter = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
const { typedrouter } = setupHandler(['certificates:read'], {
|
|
certProvisionFunction: async () => 'http01',
|
|
certProvisionScheduler: {
|
|
getBackoffInfo: async (domain: string) => domain === 'stack.gallery'
|
|
? { failures: 11, retryAfter, lastError }
|
|
: null,
|
|
},
|
|
routes: [
|
|
{
|
|
name: 'stack-gallery',
|
|
match: { domains: ['stack.gallery'] },
|
|
action: {
|
|
tls: {
|
|
mode: 'terminate',
|
|
certificate: 'auto',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await fireTypedRequest(typedrouter, 'getCertificateOverview', {
|
|
apiToken: 'valid-token',
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.response.summary.failed).toEqual(1);
|
|
expect(result.response.certificates[0].status).toEqual('failed');
|
|
expect(result.response.certificates[0].error).toEqual(lastError);
|
|
expect(result.response.certificates[0].backoffInfo.failures).toEqual(11);
|
|
});
|
|
|
|
tap.test('cleanup test db', async () => {
|
|
const testDb = await testDbPromise;
|
|
await testDb.cleanup();
|
|
});
|
|
|
|
export default tap.start();
|