morphit/scripts/collect-install-inputs-smoke.ts
2026-08-10 19:33:10 -07:00

137 lines
7.5 KiB
TypeScript

/**
* collect-install-inputs-smoke.ts (cp600) — pins the home/VPS branch: the
* per-field validators, and the collectInstallInputs flow driven with SCRIPTED
* answers (home path asks DDNS + generates two different DB passwords; vps path
* skips DDNS; a bad answer re-prompts). Every prompt must show an example and
* validate — Ken's rule — so this checks both.
*/
import {
validateDomain,
validateAcmeEmail,
validateDdnsUrl,
validateMatrixAddress,
matrixToContactUrl,
validateInstallInputs
} from '../apps/ops-cli/src/init/ansibleVars.ts';
import { collectInstallInputs, askInstallMode, type CollectDeps } from '../apps/ops-cli/src/init/collectInstallInputs.ts';
let passed = 0;
let failed = 0;
function check(name: string, cond: boolean): void {
if (cond) {
passed++;
console.log(` \u2713 ${name}`);
} else {
failed++;
console.log(` \u2717 ${name}`);
}
}
interface DriverState {
prompts: string[];
printed: string[];
exampleSets: number;
}
function driver(opts: { choice: number; answers: string[] }): { deps: CollectDeps; state: DriverState } {
const answers = [...opts.answers];
const state: DriverState = { prompts: [], printed: [], exampleSets: 0 };
const deps: CollectDeps = {
askChoice: (async () => opts.choice) as unknown as CollectDeps['askChoice'],
ask: (async (q: string) => {
state.prompts.push(q);
return answers.shift() ?? '';
}) as unknown as CollectDeps['ask'],
examples: ((_items: readonly string[]) => {
state.exampleSets += 1;
}) as unknown as CollectDeps['examples'],
print: (str: string) => {
state.printed.push(str);
}
};
return { deps, state };
}
async function main(): Promise<void> {
console.log('\u2500\u2500 collect-install-inputs smoke (cp600) \u2500\u2500\u2500\u2500');
// ── per-field validators ──────────────────────────────────────
check('domain: accepts a bare domain', validateDomain('trade.example.com') === true);
check('domain: rejects a URL', validateDomain('https://trade.example.com') !== true);
check('domain: rejects a bare hostname (no TLD)', validateDomain('localhost') !== true);
check('email: accepts a normal address', validateAcmeEmail('you@example.com') === true);
check('email: rejects nonsense', validateAcmeEmail('nope') !== true);
check('ddns: accepts https + {ip}', validateDdnsUrl('https://njal.la/update/?a={ip}') === true);
check('ddns: ACCEPTS missing {ip} (provider auto-detects, e.g. Namecheap)', validateDdnsUrl('https://njal.la/update/?a=1.2.3.4') === true);
check('ddns: rejects non-https', validateDdnsUrl('http://njal.la/update/?a={ip}') !== true);
// Matrix contact (optional link on the /instances card) — account OR room.
check('matrix: empty is OK (optional \u2014 operator may want none)', validateMatrixAddress('') === true);
check('matrix: accepts @you:matrix.org (account)', validateMatrixAddress('@you:matrix.org') === true);
check('matrix: accepts #room:matrix.org (room)', validateMatrixAddress('#support:matrix.org') === true);
check('matrix: rejects a bare handle (no @ or #)', validateMatrixAddress('you:matrix.org') !== true);
check('matrix: rejects @you (no server)', validateMatrixAddress('@you') !== true);
check('matrix: rejects #room (no server)', validateMatrixAddress('#support') !== true);
check('matrix: rejects a server with no TLD', validateMatrixAddress('@you:localhost') !== true);
check('matrix: account \u2192 matrix.to URL', matrixToContactUrl('@you:matrix.org') === 'https://matrix.to/#/@you:matrix.org');
check('matrix: room \u2192 matrix.to URL (keeps the #)', matrixToContactUrl('#support:matrix.org') === 'https://matrix.to/#/#support:matrix.org');
// askInstallMode is now asked on its own, BEFORE the numbered steps, so the
// wizard can show an accurate "Step N of {total}" from step 1 (home + vps differ).
check('askInstallMode: choice 0 → home', (await askInstallMode({ askChoice: (async () => 0) as unknown as CollectDeps['askChoice'] })) === 'home');
check('askInstallMode: choice 1 → vps', (await askInstallMode({ askChoice: (async () => 1) as unknown as CollectDeps['askChoice'] })) === 'vps');
const known = { torOnly: false, operatorAccount: 'my-operator', operatorTag: 'myoperator', feesAccount: 'my-operator', keystorePath: '/etc/morphit/relay.keystore' };
// HOME path: choice 0 → asks domain, title, description, email, ddns.
{
const d = driver({ choice: 0, answers: ['trade.example.com', 'Morphit Berlin', 'Berlin node, no KYC.', '@berlin:matrix.org', 'me@example.com', 'https://njal.la/update/?h=trade.example.com&k=K&a={ip}'] });
const out = await collectInstallInputs({ ...known, mode: 'home' }, d.deps);
check('home: mode is home', out.mode === 'home');
check('home: carries the DDNS url', out.ddnsUpdateUrl === 'https://njal.la/update/?h=trade.example.com&k=K&a={ip}');
check('home: domain + email captured', out.domain === 'trade.example.com' && out.acmeEmail === 'me@example.com');
check('home: instance title + description captured', out.instanceName === 'Morphit Berlin' && out.instanceTagline === 'Berlin node, no KYC.');
check('home: Matrix account \u2192 matrix.to contactUrl', out.contactUrl === 'https://matrix.to/#/@berlin:matrix.org');
check('home: a strong DB password generated', out.indexerDbPassword.length >= 43);
check('home: an example shown for every free-form prompt (domain, title, description, matrix, email, ddns = 6)', d.state.exampleSets === 6);
check('home: the whole result passes validateInstallInputs', validateInstallInputs(out).length === 0);
check('home: carries operator account/tag from the wizard', out.operatorAccount === 'my-operator' && out.operatorTag === 'myoperator');
}
// VPS path: choice 1 → asks domain, title, description, email; NO ddns.
{
const d = driver({ choice: 1, answers: ['trade.example.com', 'Morphit Test', '', '', 'me@example.com'] });
const out = await collectInstallInputs({ ...known, mode: 'vps' }, d.deps);
check('vps: mode is vps', out.mode === 'vps');
check('vps: NO DDNS url', out.ddnsUpdateUrl === undefined);
check('vps: empty description → undefined tagline (title still captured)', out.instanceTagline === undefined && out.instanceName === 'Morphit Test');
check('vps: empty Matrix → undefined contactUrl (link omitted, no failure)', out.contactUrl === undefined);
check('vps: 5 example sets (domain, title, description, matrix, email — no ddns)', d.state.exampleSets === 5);
check('vps: passes validateInstallInputs', validateInstallInputs(out).length === 0);
}
// RE-PROMPT: a bad domain then a good one → domain asked twice, printed an error.
{
const d = driver({ choice: 1, answers: ['not a domain', 'trade.example.com', 'Morphit Test', '', '', 'me@example.com'] });
const out = await collectInstallInputs({ ...known, mode: 'vps' }, d.deps);
const domainPrompts = d.state.prompts.filter((p) => /domain/i.test(p)).length;
check('re-prompt: a bad domain makes the domain question repeat', domainPrompts >= 2);
check('re-prompt: an error line was printed', d.state.printed.some((s) => /Try again/.test(s)));
check('re-prompt: still ends with valid inputs', validateInstallInputs(out).length === 0 && out.domain === 'trade.example.com');
}
}
main()
.then(() => {
console.log('');
if (failed === 0) {
console.log(`\u2713 all ${passed} collect-install-inputs checks passed`);
process.exit(0);
} else {
console.log(`\u2717 ${failed} of ${passed + failed} collect-install-inputs checks failed`);
process.exit(1);
}
})
.catch((e) => {
console.error(e);
process.exit(1);
});