import 'dart:io'; import 'package:test/test.dart'; /// halkyon.li's first start (deploy/host/first-start.md, ADR 0039) against what the /// compose files run: the sidecars' users as their containers run, their folders /// where the containers mount them, and a role and database for each app's server. void main() { final steps = File('deploy/host/first-start.md').readAsStringSync(); String compose(String name) => File('deploy/$name').readAsStringSync(); /// The users step 1 makes, by name: their IDs. final users = { for (final match in RegExp( r'(\w+):(\d{5})', ).allMatches(RegExp('for each in (.+); do').firstMatch(steps)![1]!)) match[1]!: match[2]!, }; test("makes each sidecar's user with the ID its container runs as", () { const runs = { 'jellyfin': 'compose.jellyfin.yaml', 'languagetool': 'compose.languagetool.yaml', 'brouter': 'compose.train.yaml', 'maptiles': 'compose.maps.yaml', 'photon': 'compose.search.yaml', }; expect(users.keys, unorderedEquals(runs.keys)); for (final MapEntry(key: name, value: file) in runs.entries) { final id = users[name]!; expect(compose(file), contains('user: "$id:$id"'), reason: name); } }); test("never gives a sidecar the runner's ID, nor one ID twice", () { expect(users.values, isNot(contains('61000'))); expect(users.values.toSet(), hasLength(users.length)); }); test("makes each sidecar's folder its own, where its container mounts " 'it', () { const folders = { 'languagetool': (r'$d/languagetool/fasttext', './languagetool/fasttext'), 'brouter': (r'$d/brouter/segments', './brouter/segments'), 'maptiles': (r'$d/maps/tiles', './maps/tiles'), 'photon': (r'$d/photon/data', './photon/data'), }; final mounted = [ for (final file in [ 'compose.languagetool.yaml', 'compose.train.yaml', 'compose.maps.yaml', 'compose.search.yaml', ]) compose(file), ].join('\n'); for (final MapEntry(key: owner, value: (made, mount)) in folders.entries) { expect( RegExp('sudo install -d -o $owner -g $owner .*${RegExp.escape(made)}') .hasMatch(steps), isTrue, reason: made, ); expect(mounted, contains(mount), reason: mount); } }); test("gives each app's server a role of its own, owning its database " 'alone, and lets no role connect to a database not its own', () { for (final app in ['train', 'todo', 'meditate']) { expect( steps, contains("CREATE ROLE halkyon_$app LOGIN PASSWORD :'$app';"), reason: app, ); expect( steps, contains('CREATE DATABASE halkyon_$app OWNER halkyon_$app;'), reason: app, ); expect( compose('compose.$app.yaml'), contains( r'${' '${app.toUpperCase()}_POSTGRES_PASSWORD', ), reason: app, ); } for (final app in ['learn', 'train', 'todo', 'meditate']) { expect( steps, contains('REVOKE CONNECT ON DATABASE halkyon_$app FROM PUBLIC;'), reason: app, ); } expect(steps, isNot(contains('SUPERUSER'))); expect(steps, isNot(contains('CREATEDB'))); }); test("pins the developer's key on deploy to redeploy.sh alone", () { expect( steps, contains('restrict,command="/home/deploy/halkyon/deploy/redeploy.sh" %s'), ); }); }