BREAKING CHANGE(SmartAcme): Refactor challenge handling by removing legacy setChallenge/removeChallenge in favor of pluggable challengeHandlers and update documentation and tests accordingly
This commit is contained in:
21
test/test.certmatcher.ts
Normal file
21
test/test.certmatcher.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { SmartacmeCertMatcher } from '../ts/smartacme.classes.certmatcher.js';
|
||||
|
||||
tap.test('should match 2-level domain', async () => {
|
||||
const matcher = new SmartacmeCertMatcher();
|
||||
expect(matcher.getCertificateDomainNameByDomainName('example.com')).toEqual('example.com');
|
||||
});
|
||||
|
||||
tap.test('should match 3-level domain', async () => {
|
||||
const matcher = new SmartacmeCertMatcher();
|
||||
expect(matcher.getCertificateDomainNameByDomainName('subdomain.example.com')).toEqual('example.com');
|
||||
});
|
||||
|
||||
tap.test('should return undefined for deeper domain', async () => {
|
||||
const matcher = new SmartacmeCertMatcher();
|
||||
// domain with 4 or more levels
|
||||
const result = matcher.getCertificateDomainNameByDomainName('a.b.example.com');
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
export default tap.start();
|
38
test/test.handlers-dns01.ts
Normal file
38
test/test.handlers-dns01.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
|
||||
|
||||
tap.test('Dns01Handler prepare and cleanup calls Cloudflare and DNS functions', async () => {
|
||||
let setCalled = false;
|
||||
let removeCalled = false;
|
||||
// fake Cloudflare API
|
||||
const fakeCF: any = {
|
||||
convenience: {
|
||||
acmeSetDnsChallenge: async (ch: any) => {
|
||||
setCalled = true;
|
||||
expect(ch).toHaveProperty('hostName');
|
||||
expect(ch).toHaveProperty('challenge');
|
||||
},
|
||||
acmeRemoveDnsChallenge: async (ch: any) => {
|
||||
removeCalled = true;
|
||||
expect(ch).toHaveProperty('hostName');
|
||||
expect(ch).toHaveProperty('challenge');
|
||||
},
|
||||
},
|
||||
};
|
||||
// fake DNS checker
|
||||
const fakeDNS: any = {
|
||||
checkUntilAvailable: async (host: string, rr: string, val: string, count: number, interval: number) => {
|
||||
expect(host).toEqual('test.host');
|
||||
expect(rr).toEqual('TXT');
|
||||
expect(val).toEqual('token');
|
||||
},
|
||||
};
|
||||
const handler = new Dns01Handler(fakeCF, fakeDNS);
|
||||
const input = { hostName: 'test.host', challenge: 'token' };
|
||||
await handler.prepare(input);
|
||||
expect(setCalled).toEqual(true);
|
||||
await handler.cleanup(input);
|
||||
expect(removeCalled).toEqual(true);
|
||||
});
|
||||
|
||||
export default tap.start();
|
26
test/test.handlers-http01.ts
Normal file
26
test/test.handlers-http01.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Http01Handler } from '../ts/handlers/Http01Handler.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
tap.test('Http01Handler writes challenge file and removes it on cleanup', async () => {
|
||||
// create temporary webroot directory
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'http01-'));
|
||||
const handler = new Http01Handler({ webroot: tmpDir });
|
||||
const token = 'testtoken';
|
||||
const keyAuth = 'keyAuthValue';
|
||||
const webPath = `/.well-known/acme-challenge/${token}`;
|
||||
const input = { type: 'http-01', token, keyAuthorization: keyAuth, webPath };
|
||||
// prepare should write the file
|
||||
await handler.prepare(input);
|
||||
const filePath = path.join(tmpDir, webPath);
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
expect(content).toEqual(keyAuth);
|
||||
// cleanup should remove the file
|
||||
await handler.cleanup(input);
|
||||
const exists = await fs.stat(filePath).then(() => true).catch(() => false);
|
||||
expect(exists).toEqual(false);
|
||||
});
|
||||
|
||||
export default tap.start();
|
47
test/test.smartacme.integration.ts
Normal file
47
test/test.smartacme.integration.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
import { SmartAcme } from '../ts/index.js';
|
||||
import { Dns01Handler } from '../ts/handlers/Dns01Handler.js';
|
||||
|
||||
// Load environment variables for credentials (stored under .nogit/)
|
||||
const testQenv = new Qenv('./', './.nogit/');
|
||||
// Cloudflare API token for DNS-01 challenge (must be set in .nogit/ or env)
|
||||
const cfToken = (await testQenv.getEnvVarOnDemand('CF_TOKEN'))!;
|
||||
const cfAccount = new cloudflare.CloudflareAccount(cfToken);
|
||||
// MongoDB connection settings for certificate storage (must be set in .nogit/ or env)
|
||||
const mongoDbName = (await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'))!;
|
||||
const mongoDbPass = (await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'))!;
|
||||
const mongoDbUrl = (await testQenv.getEnvVarOnDemand('MONGODB_URL'))!;
|
||||
|
||||
let smartAcmeInstance: SmartAcme;
|
||||
|
||||
tap.test('create SmartAcme instance with DNS-01 handler and start', async () => {
|
||||
smartAcmeInstance = new SmartAcme({
|
||||
accountEmail: 'domains@lossless.org',
|
||||
mongoDescriptor: { mongoDbName, mongoDbPass, mongoDbUrl },
|
||||
environment: 'integration',
|
||||
retryOptions: {},
|
||||
challengeHandlers: [new Dns01Handler(cfAccount)],
|
||||
challengePriority: ['dns-01'],
|
||||
});
|
||||
await smartAcmeInstance.start();
|
||||
expect(smartAcmeInstance).toBeInstanceOf(SmartAcme);
|
||||
});
|
||||
|
||||
tap.test('get a domain certificate via DNS-01 challenge', async () => {
|
||||
// Replace 'bleu.de' with your test domain if different
|
||||
const domain = 'bleu.de';
|
||||
const cert = await smartAcmeInstance.getCertificateForDomain(domain);
|
||||
expect(cert).toHaveProperty('domainName');
|
||||
expect(cert.domainName).toEqual(domain);
|
||||
expect(cert).toHaveProperty('publicKey');
|
||||
expect(typeof cert.publicKey).toEqual('string');
|
||||
expect(cert.publicKey.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('stop SmartAcme instance', async () => {
|
||||
await smartAcmeInstance.stop();
|
||||
});
|
||||
|
||||
export default tap.start();
|
32
test/test.smartacme.ts
Normal file
32
test/test.smartacme.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { SmartAcme } from '../ts/index.js';
|
||||
import type { IChallengeHandler } from '../ts/handlers/IChallengeHandler.js';
|
||||
|
||||
// Dummy handler for testing
|
||||
class DummyHandler implements IChallengeHandler<any> {
|
||||
getSupportedTypes(): string[] { return ['dns-01']; }
|
||||
async prepare(_: any): Promise<void> { /* no-op */ }
|
||||
async cleanup(_: any): Promise<void> { /* no-op */ }
|
||||
}
|
||||
|
||||
tap.test('constructor throws without challengeHandlers', async () => {
|
||||
expect(() => new SmartAcme({
|
||||
accountEmail: 'test@example.com',
|
||||
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
|
||||
environment: 'integration',
|
||||
retryOptions: {},
|
||||
} as any)).toThrow();
|
||||
});
|
||||
|
||||
tap.test('constructor accepts valid challengeHandlers', async () => {
|
||||
const sa = new SmartAcme({
|
||||
accountEmail: 'test@example.com',
|
||||
mongoDescriptor: { mongoDbName: 'db', mongoDbPass: 'pass', mongoDbUrl: 'url' },
|
||||
environment: 'integration',
|
||||
retryOptions: {},
|
||||
challengeHandlers: [new DummyHandler()],
|
||||
});
|
||||
expect(sa).toBeInstanceOf(SmartAcme);
|
||||
});
|
||||
|
||||
export default tap.start();
|
48
test/test.ts
48
test/test.ts
@ -1,48 +0,0 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Qenv } from '@push.rocks/qenv';
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
|
||||
const testQenv = new Qenv('./', './.nogit/');
|
||||
const testCloudflare = new cloudflare.CloudflareAccount(await testQenv.getEnvVarOnDemand('CF_TOKEN'));
|
||||
|
||||
import * as smartacme from '../ts/index.js';
|
||||
|
||||
let smartAcmeInstance: smartacme.SmartAcme;
|
||||
|
||||
tap.test('should create a valid instance of SmartAcme', async () => {
|
||||
smartAcmeInstance = new smartacme.SmartAcme({
|
||||
accountEmail: 'domains@lossless.org',
|
||||
accountPrivateKey: null,
|
||||
mongoDescriptor: {
|
||||
mongoDbName: await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
|
||||
mongoDbPass: await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
|
||||
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL'),
|
||||
},
|
||||
removeChallenge: async (dnsChallenge) => {
|
||||
testCloudflare.convenience.acmeRemoveDnsChallenge(dnsChallenge);
|
||||
},
|
||||
setChallenge: async (dnsChallenge) => {
|
||||
testCloudflare.convenience.acmeSetDnsChallenge(dnsChallenge);
|
||||
},
|
||||
environment: 'integration',
|
||||
});
|
||||
await smartAcmeInstance.start();
|
||||
});
|
||||
|
||||
tap.test('should get a domain certificate', async () => {
|
||||
const certificate = await smartAcmeInstance.getCertificateForDomain('bleu.de');
|
||||
console.log(certificate);
|
||||
});
|
||||
|
||||
tap.test('certmatcher should correctly match domains', async () => {
|
||||
const certMatcherMod = await import('../ts/smartacme.classes.certmatcher.js');
|
||||
const certMatcher = new certMatcherMod.SmartacmeCertMatcher();
|
||||
const matchedCert = certMatcher.getCertificateDomainNameByDomainName('level3.level2.level1');
|
||||
expect(matchedCert).toEqual('level2.level1');
|
||||
});
|
||||
|
||||
tap.test('should stop correctly', async () => {
|
||||
await smartAcmeInstance.stop();
|
||||
});
|
||||
|
||||
tap.start();
|
Reference in New Issue
Block a user