Files
menu/test/site-configs.test.ts
T
tanshu 01ce0bd609 Create each Site's network with its own subnet
The deploy failed on hngmohali with 'all predefined address pools have been
fully subnetted': every 172.17-172.30/16 is in use, 172.31/16 is the host's own
VPC so Docker will not allocate from it, and all sixteen 192.168.0-240/20 blocks
are taken. Docker therefore refuses a network created without a subnet.

Each Site now names a free network_subnet (10.210.0.0/16, one /24 each), the role
looks the network up first and only creates it when it is absent and with that
subnet, and it fails with an explanation when a Site declares none. Existing
networks are left alone, because Docker cannot change a subnet in place and the
Sites already running have theirs. The test checks every Site declares one, and
the deployment readme says a new Site needs a free subnet.

deploy.sh now deploys every Site by default, with SITE=<name> to narrow it, which
is what the local edit was reaching for.
2026-09-23 00:18:04 +05:30

69 lines
2.9 KiB
TypeScript

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { describe, it } from 'node:test';
import { parseSiteConfig } from '../src/config.ts';
const root = path.join(import.meta.dirname, '..');
/** The inventory decides which Sites exist; the playbook names the vars file. */
async function inventory(): Promise<{ name: string; varFile: string }[]> {
const text = await readFile(path.join(root, 'ansible', 'hosts'), 'utf8');
const inMenus = text.split(/^\[/m).find((block) => block.startsWith('menus]')) ?? '';
return inMenus
.split('\n')
.slice(1)
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'))
.map((line) => {
const [name, ...vars] = line.split(/\s+/);
const varFile = vars.find((entry) => entry.startsWith('var_file=')) ?? '';
return { name: name!, varFile: varFile.replace('var_file=', '') };
});
}
describe('every Site in the inventory is complete', () => {
it('has a config file, a vars file, and a config that parses', async () => {
const sites = await inventory();
assert.ok(sites.length >= 3, `expected the three Sites, found ${sites.length}`);
for (const { name, varFile } of sites) {
assert.notEqual(varFile, '', `${name} names no var_file`);
const configPath = path.join(root, 'config', `${name}.site.json`);
const raw = JSON.parse(await readFile(configPath, 'utf8'));
const config = parseSiteConfig(raw);
assert.equal(
config.site.startsWith('menu.'),
true,
`${name}: site should be a menu hostname`,
);
assert.ok(config.sections.length > 0, `${name} seeds no categories`);
assert.match(
config.theme.steel,
/^#[0-9a-f]{6}$/i,
`${name}: the theme should carry real colours`,
);
const varsRaw = await readFile(path.join(root, 'ansible', varFile), 'utf8');
// Each vars file only needs to say where the Site runs; the templating is
// quoted, which is what a first deploy got wrong.
assert.match(varsRaw, /^title: /m, `${name}: ${varFile} has no title`);
assert.match(varsRaw, /^host: 'menu\.\{\{ title \}\}'/m, `${name}: host is not quoted`);
assert.match(varsRaw, /^app_root: /m, `${name}: no app_root`);
// Without this the play fails on a host whose Docker pools are full.
assert.match(varsRaw, /^network_subnet: /m, `${name}: no network_subnet`);
}
});
it('leaves no Site config without an inventory entry', async () => {
const sites = new Set((await inventory()).map((site) => site.name));
const files = await readdir(path.join(root, 'config'));
for (const file of files.filter((entry) => entry.endsWith('.site.json'))) {
const name = file.replace('.site.json', '');
assert.ok(sites.has(name), `config/${file} belongs to no Site in the inventory`);
}
});
});