The 10.maj.2026 force-renew run reloaded nginx twice back-to-back: once because acme.sh's --renew ran the saved Le_ReloadCmd, and again because setup/tls.ts unconditionally followed it with --install-cert. Short-circuit the second call when Le_RealKeyPath, Le_RealFullchainPath, and Le_ReloadCmd in the domain conf already match our canonical values; first issue and no-prior-conf force-issue paths still install as before. Also: per-cert failures no longer strand the rest of the batch. The run loop aggregates failures, still installs the renewal cron, then exits 1 with FAILED_LABELS surfaced in the status line. --- Build: FAIL | Tests: FAIL — 16 failed
220 lines
6.1 KiB
TypeScript
220 lines
6.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
DEFAULT_MANAGED_CERTS,
|
|
acmeInstallPathsAreCanonical,
|
|
buildAcmeInstallCertArgs,
|
|
buildAcmeIssueArgs,
|
|
buildAcmeRenewArgs,
|
|
parseAcmeDomainConf,
|
|
parseArgs,
|
|
type ManagedCert,
|
|
} from './tls.js';
|
|
|
|
const SAMPLE: ManagedCert = {
|
|
label: 'clawdie',
|
|
primaryDomain: 'clawdie.si',
|
|
altDomains: ['www.clawdie.si'],
|
|
webroot: '/usr/local/www/clawdie',
|
|
};
|
|
|
|
describe('parseArgs', () => {
|
|
it('defaults to dry-run with no email and no label filter', () => {
|
|
const before = process.env.ACME_EMAIL;
|
|
delete process.env.ACME_EMAIL;
|
|
try {
|
|
expect(parseArgs([])).toEqual({
|
|
dryRun: true,
|
|
email: null,
|
|
onlyLabel: null,
|
|
forceRenew: false,
|
|
smokeTest: false,
|
|
});
|
|
} finally {
|
|
if (before !== undefined) process.env.ACME_EMAIL = before;
|
|
}
|
|
});
|
|
|
|
it('--apply flips dry-run off', () => {
|
|
expect(parseArgs(['--apply']).dryRun).toBe(false);
|
|
});
|
|
|
|
it('--cert <label> narrows to a single cert', () => {
|
|
expect(parseArgs(['--cert', 'docs']).onlyLabel).toBe('docs');
|
|
});
|
|
|
|
it('--email overrides $ACME_EMAIL', () => {
|
|
expect(parseArgs(['--email', 'ops@example.com']).email).toBe(
|
|
'ops@example.com',
|
|
);
|
|
});
|
|
|
|
it('--force-renew is captured', () => {
|
|
expect(parseArgs(['--force-renew']).forceRenew).toBe(true);
|
|
});
|
|
|
|
it('--smoke-test stays non-mutating', () => {
|
|
expect(parseArgs(['--smoke-test'])).toMatchObject({
|
|
dryRun: true,
|
|
smokeTest: true,
|
|
});
|
|
});
|
|
|
|
it('rejects combining --smoke-test with --apply', () => {
|
|
expect(() => parseArgs(['--smoke-test', '--apply'])).toThrow(/smoke-test/);
|
|
});
|
|
|
|
it('rejects unknown args so typos fail loud', () => {
|
|
expect(() => parseArgs(['--apply-now'])).toThrow(/unknown argument/i);
|
|
});
|
|
|
|
it('rejects --email without a value', () => {
|
|
expect(() => parseArgs(['--email'])).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('buildAcmeIssueArgs', () => {
|
|
it('builds a -d-per-domain arg list with -w webroot', () => {
|
|
expect(buildAcmeIssueArgs(SAMPLE)).toEqual([
|
|
'--issue',
|
|
'--server',
|
|
'letsencrypt',
|
|
'-d',
|
|
'clawdie.si',
|
|
'-d',
|
|
'www.clawdie.si',
|
|
'-w',
|
|
'/usr/local/www/clawdie',
|
|
]);
|
|
});
|
|
|
|
it('omits SAN -d entries when altDomains is empty', () => {
|
|
const single: ManagedCert = { ...SAMPLE, altDomains: [] };
|
|
expect(buildAcmeIssueArgs(single)).toEqual([
|
|
'--issue',
|
|
'--server',
|
|
'letsencrypt',
|
|
'-d',
|
|
'clawdie.si',
|
|
'-w',
|
|
'/usr/local/www/clawdie',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('buildAcmeRenewArgs', () => {
|
|
it('uses the primary domain and letsencrypt server', () => {
|
|
expect(buildAcmeRenewArgs(SAMPLE, false)).toEqual([
|
|
'--renew',
|
|
'--server',
|
|
'letsencrypt',
|
|
'-d',
|
|
'clawdie.si',
|
|
]);
|
|
});
|
|
|
|
it('adds --force only when requested', () => {
|
|
expect(buildAcmeRenewArgs(SAMPLE, true)).toContain('--force');
|
|
});
|
|
});
|
|
|
|
describe('buildAcmeInstallCertArgs', () => {
|
|
it('writes to /usr/local/etc/nginx/ssl/<label>/ with the nginx reload hook', () => {
|
|
expect(buildAcmeInstallCertArgs(SAMPLE)).toEqual([
|
|
'--install-cert',
|
|
'-d',
|
|
'clawdie.si',
|
|
'--key-file',
|
|
'/usr/local/etc/nginx/ssl/clawdie/clawdie.key',
|
|
'--fullchain-file',
|
|
'/usr/local/etc/nginx/ssl/clawdie/fullchain.cer',
|
|
'--reloadcmd',
|
|
'service nginx reload',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('parseAcmeDomainConf', () => {
|
|
it('strips single quotes and ignores comments/blank lines', () => {
|
|
const conf = parseAcmeDomainConf(
|
|
[
|
|
'# acme.sh domain conf',
|
|
'',
|
|
"Le_Domain='clawdie.si'",
|
|
"Le_RealKeyPath='/usr/local/etc/nginx/ssl/clawdie/clawdie.key'",
|
|
"Le_RealFullchainPath='/usr/local/etc/nginx/ssl/clawdie/fullchain.cer'",
|
|
"Le_ReloadCmd='service nginx reload'",
|
|
].join('\n'),
|
|
);
|
|
expect(conf.Le_Domain).toBe('clawdie.si');
|
|
expect(conf.Le_RealKeyPath).toBe(
|
|
'/usr/local/etc/nginx/ssl/clawdie/clawdie.key',
|
|
);
|
|
expect(conf.Le_ReloadCmd).toBe('service nginx reload');
|
|
});
|
|
|
|
it('handles unquoted values', () => {
|
|
expect(parseAcmeDomainConf('Le_Keylength=ec-256\n').Le_Keylength).toBe(
|
|
'ec-256',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('acmeInstallPathsAreCanonical', () => {
|
|
const sample: ManagedCert = {
|
|
label: 'clawdie',
|
|
primaryDomain: 'clawdie.si',
|
|
altDomains: [],
|
|
webroot: '/usr/local/www/clawdie',
|
|
};
|
|
|
|
it('returns true when key/fullchain/reload all match', () => {
|
|
expect(
|
|
acmeInstallPathsAreCanonical(sample, {
|
|
Le_RealKeyPath: '/usr/local/etc/nginx/ssl/clawdie/clawdie.key',
|
|
Le_RealFullchainPath: '/usr/local/etc/nginx/ssl/clawdie/fullchain.cer',
|
|
Le_ReloadCmd: 'service nginx reload',
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('returns false when reload cmd was customized', () => {
|
|
expect(
|
|
acmeInstallPathsAreCanonical(sample, {
|
|
Le_RealKeyPath: '/usr/local/etc/nginx/ssl/clawdie/clawdie.key',
|
|
Le_RealFullchainPath: '/usr/local/etc/nginx/ssl/clawdie/fullchain.cer',
|
|
Le_ReloadCmd: 'service nginx restart',
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('returns false when fullchain path drifted', () => {
|
|
expect(
|
|
acmeInstallPathsAreCanonical(sample, {
|
|
Le_RealKeyPath: '/usr/local/etc/nginx/ssl/clawdie/clawdie.key',
|
|
Le_RealFullchainPath: '/etc/ssl/clawdie/fullchain.cer',
|
|
Le_ReloadCmd: 'service nginx reload',
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('returns false when conf is missing the install fields entirely', () => {
|
|
expect(acmeInstallPathsAreCanonical(sample, {})).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('DEFAULT_MANAGED_CERTS', () => {
|
|
it('includes both clawdie.si (with www SAN) and docs.clawdie.si', () => {
|
|
const labels = DEFAULT_MANAGED_CERTS.map((c) => c.label);
|
|
expect(labels).toEqual(['clawdie', 'docs']);
|
|
const clawdie = DEFAULT_MANAGED_CERTS.find((c) => c.label === 'clawdie');
|
|
expect(clawdie?.altDomains).toContain('www.clawdie.si');
|
|
});
|
|
|
|
it('every cert has a non-empty primary domain and webroot', () => {
|
|
for (const cert of DEFAULT_MANAGED_CERTS) {
|
|
expect(cert.primaryDomain).toMatch(/\./);
|
|
expect(cert.webroot.startsWith('/')).toBe(true);
|
|
}
|
|
});
|
|
});
|