clawdie-ai/setup/tls.test.ts
Operator & Claude Code bbff424272 Skip redundant install-cert after acme.sh --renew
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
2026-05-10 16:56:52 +02:00

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);
}
});
});