BREAKING CHANGE(smartacme): Make wildcard certificates opt-in to fix HTTP-01 only configurations

This commit is contained in:
2025-05-19 10:01:31 +00:00
parent dcc89f0088
commit 086eea1aa2
8 changed files with 359 additions and 11 deletions

178
test/test.http01-only.ts Normal file
View File

@ -0,0 +1,178 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme, certmanagers } from '../ts/index.js';
import { Http01MemoryHandler } from '../ts/handlers/Http01MemoryHandler.js';
// Test that HTTP-01 only configuration works without wildcard certificates
tap.test('HTTP-01 only configuration should work for regular domains', async () => {
const memHandler = new Http01MemoryHandler();
// Stub the domain support check to always return true for testing
memHandler.checkWetherDomainIsSupported = async () => true;
const smartAcmeInstance = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [memHandler],
challengePriority: ['http-01'],
});
// Stub the start method to avoid actual ACME connections
smartAcmeInstance.start = async () => {
smartAcmeInstance.certmatcher = {
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
} as any;
smartAcmeInstance.interestMap = {
checkInterest: async () => false,
addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
} as any;
await smartAcmeInstance.certmanager.init();
};
await smartAcmeInstance.start();
// Stub the core certificate methods to avoid actual ACME calls
smartAcmeInstance.client = {
createOrder: async (orderPayload: any) => {
// Verify no wildcard is included in default request
const identifiers = orderPayload.identifiers;
expect(identifiers.length).toEqual(1);
expect(identifiers[0].value).toEqual('example.com');
expect(identifiers.find((id: any) => id.value.startsWith('*.'))).toBeUndefined();
return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
},
getAuthorizations: async () => [],
finalizeOrder: async () => {},
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
} as any;
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
// Mock certmanager methods
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
// Request certificate without wildcard
const cert = await smartAcmeInstance.getCertificateForDomain('example.com');
expect(cert).toBeDefined();
});
tap.test('should only include wildcard when explicitly requested with DNS-01', async () => {
const dnsHandler = {
getSupportedTypes: () => ['dns-01'],
prepare: async () => {},
cleanup: async () => {},
checkWetherDomainIsSupported: async () => true,
};
const smartAcmeInstance = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [dnsHandler],
challengePriority: ['dns-01'],
});
// Stub the start method to avoid actual ACME connections
smartAcmeInstance.start = async () => {
smartAcmeInstance.certmatcher = {
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
} as any;
smartAcmeInstance.interestMap = {
checkInterest: async () => false,
addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
} as any;
await smartAcmeInstance.certmanager.init();
};
await smartAcmeInstance.start();
// Stub the core certificate methods
smartAcmeInstance.client = {
createOrder: async (orderPayload: any) => {
const identifiers = orderPayload.identifiers;
expect(identifiers.length).toEqual(2);
expect(identifiers[0].value).toEqual('example.com');
expect(identifiers[1].value).toEqual('*.example.com');
return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
},
getAuthorizations: async () => [],
finalizeOrder: async () => {},
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
} as any;
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
// Mock certmanager methods
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
// Request certificate with wildcard
const cert = await smartAcmeInstance.getCertificateForDomain('example.com', { includeWildcard: true });
expect(cert).toBeDefined();
});
tap.test('should skip wildcard if requested but no DNS-01 handler available', async () => {
const httpHandler = new Http01MemoryHandler();
httpHandler.checkWetherDomainIsSupported = async () => true;
const smartAcmeInstance = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
retryOptions: {},
challengeHandlers: [httpHandler],
challengePriority: ['http-01'],
});
// Stub the start method to avoid actual ACME connections
smartAcmeInstance.start = async () => {
smartAcmeInstance.certmatcher = {
getCertificateDomainNameByDomainName: (domain: string) => domain.replace('*.', '')
} as any;
smartAcmeInstance.interestMap = {
checkInterest: async () => false,
addInterest: async () => ({ interestFullfilled: new Promise(() => {}) } as any)
} as any;
await smartAcmeInstance.certmanager.init();
};
await smartAcmeInstance.start();
// Mock logger to capture warning
const logSpy = { called: false, message: '' };
smartAcmeInstance.logger.log = async (level: string, message: string) => {
if (level === 'warn') {
logSpy.called = true;
logSpy.message = message;
}
};
// Stub the core certificate methods
smartAcmeInstance.client = {
createOrder: async (orderPayload: any) => {
const identifiers = orderPayload.identifiers;
// Should only have regular domain, no wildcard
expect(identifiers.length).toEqual(1);
expect(identifiers[0].value).toEqual('example.com');
return { status: 'pending', authorizations: [], finalize: '', certificate: '' };
},
getAuthorizations: async () => [],
finalizeOrder: async () => {},
getCertificate: async () => '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
} as any;
smartAcmeInstance.retry = async (fn: () => Promise<any>) => fn();
// Mock certmanager methods
smartAcmeInstance.certmanager.retrieveCertificate = async () => null;
smartAcmeInstance.certmanager.storeCertificate = async (cert: any) => cert;
// Request certificate with wildcard (should be skipped)
const cert = await smartAcmeInstance.getCertificateForDomain('example.com', { includeWildcard: true });
expect(cert).toBeDefined();
expect(logSpy.called).toBeTrue();
expect(logSpy.message).toContain('Wildcard certificate requested but no DNS-01 handler available');
});
export default tap.start();

View File

@ -0,0 +1,94 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartAcme, certmanagers } from '../ts/index.js';
import { SmartacmeCert as Cert } from '../ts/smartacme.classes.cert.js';
// Simple test to verify wildcard options are correctly processed
tap.test('should not include wildcard by default for regular domain', async () => {
let orderPayload: any = null;
// Override the SmartAcme prototype methods for testing
const origGetCert = SmartAcme.prototype.getCertificateForDomain;
// Create a minimal test version of getCertificateForDomain
SmartAcme.prototype.getCertificateForDomain = async function(
domainArg: string,
options?: { includeWildcard?: boolean }
) {
const certDomainName = domainArg.replace('*.', '');
const identifiers = [];
if (domainArg.startsWith('*.')) {
identifiers.push({ type: 'dns', value: domainArg });
} else {
identifiers.push({ type: 'dns', value: certDomainName });
if (options?.includeWildcard) {
const hasDnsHandler = this.challengeHandlers.some((h) =>
h.getSupportedTypes().includes('dns-01')
);
if (hasDnsHandler) {
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
}
}
}
orderPayload = { identifiers };
return new Cert({ domainName: certDomainName });
};
try {
// Create instance with HTTP-01 only
const smartAcme = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
challengeHandlers: [{
getSupportedTypes: () => ['http-01'],
prepare: async () => {},
cleanup: async () => {},
checkWetherDomainIsSupported: async () => true,
}],
});
// Test 1: Regular domain without wildcard option
await smartAcme.getCertificateForDomain('example.com');
expect(orderPayload.identifiers.length).toEqual(1);
expect(orderPayload.identifiers[0].value).toEqual('example.com');
// Test 2: Regular domain with wildcard option (but no DNS-01 handler)
await smartAcme.getCertificateForDomain('example.com', { includeWildcard: true });
expect(orderPayload.identifiers.length).toEqual(1);
expect(orderPayload.identifiers[0].value).toEqual('example.com');
// Create instance with DNS-01
const smartAcmeDns = new SmartAcme({
accountEmail: 'test@example.com',
certManager: new certmanagers.MemoryCertManager(),
environment: 'integration',
challengeHandlers: [{
getSupportedTypes: () => ['dns-01'],
prepare: async () => {},
cleanup: async () => {},
checkWetherDomainIsSupported: async () => true,
}],
});
// Test 3: Regular domain with wildcard option and DNS-01 handler
await smartAcmeDns.getCertificateForDomain('example.com', { includeWildcard: true });
expect(orderPayload.identifiers.length).toEqual(2);
expect(orderPayload.identifiers[0].value).toEqual('example.com');
expect(orderPayload.identifiers[1].value).toEqual('*.example.com');
// Test 4: Direct wildcard request
await smartAcmeDns.getCertificateForDomain('*.example.com');
expect(orderPayload.identifiers.length).toEqual(1);
expect(orderPayload.identifiers[0].value).toEqual('*.example.com');
} finally {
// Restore original method
SmartAcme.prototype.getCertificateForDomain = origGetCert;
}
});
export default tap.start();