halkyon/test/deploy/meditate_deploy_test.dart
Tizian 「ティツィアーン」 fd9c2dfe76
feat(meditate,deploy): Meditate beside the instance, and every server's public address from .env
Meditate's deploy step (ADR 0038, step 7; ADR 0039, D14), as Todo's: the
server's Dockerfile with the library's check compiled beside it, its
production config on its own database and role, compose.meditate.yaml as a
member of the register, its site sending /library/* to the web server and
the rest to the API server, the operator's library read-only in a compose
file of its own, and redeploy.sh, backup.sh and the image replay taking it
in.

Every server's public address now comes from .env (Serverpod's
SERVERPOD_API_SERVER_PUBLIC_HOST and _WEB_SERVER_, which win over the
config file): Train, Todo and Meditate from their domains, Learn from
LEARN_PUBLIC_HOST, unset learn.halkyon.eu as before. Without it a server on
halkyon.li would name halkyon.eu in its SimpleLogin callback, profile pages
and privacy page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:59:39 +01:00

178 lines
5.8 KiB
Dart

import 'dart:io';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
import '../../tool/src/images.dart';
/// Meditate's server beside the instance (Halkyon Meditate's ADR 0038, step
/// 7; ADR 0039, D14), as Todo's: a member of the register, its own database
/// and role, its site, and the operator's library read-only (D16).
void main() {
YamlMap services(String path) =>
(loadYaml(File(path).readAsStringSync()) as YamlMap)['services']
as YamlMap;
String read(String path) => File(path).readAsStringSync();
group('its server', () {
final server =
services('deploy/compose.meditate.yaml')['meditate-server'] as YamlMap;
final environment = server['environment'] as YamlMap;
test('signs in through the register, with the instance secret', () {
expect(environment['ACCOUNT_REGISTER_URL'], 'http://server:8082/');
expect(
environment['SERVERPOD_PASSWORD_instanceSecret'],
r'${INSTANCE_SECRET:?set INSTANCE_SECRET in deploy/.env}',
);
});
test('has a database password and pepper of its own', () {
expect(
environment['SERVERPOD_DATABASE_PASSWORD'],
startsWith(r'${MEDITATE_POSTGRES_PASSWORD:?'),
);
expect(
environment['SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper'],
startsWith(r'${MEDITATE_SESSION_KEY_HASH_PEPPER:?'),
);
});
test('names its own domain as its public address', () {
for (final variable in [
'SERVERPOD_API_SERVER_PUBLIC_HOST',
'SERVERPOD_WEB_SERVER_PUBLIC_HOST',
]) {
expect(environment[variable], startsWith(r'${MEDITATE_DOMAIN:?'));
}
});
test('has no library unless its own file mounts one', () {
expect(environment.keys, isNot(contains('MEDITATE_LIBRARY_DIR')));
expect(server['volumes'], isNull);
});
test("is told to Learn's register as a member", () {
final learn =
services('deploy/compose.meditate.yaml')['server'] as YamlMap;
expect(
(learn['environment'] as YamlMap)['INSTANCE_MEMBER_MEDITATE'],
'http://meditate-server:8082/',
);
});
test('connects to its own database as its own role', () {
final config = loadYaml(
read('apps/meditate/halkyon_meditate_server/config/production.yaml'),
) as YamlMap;
final database = config['database'] as YamlMap;
expect(database['name'], 'halkyon_meditate');
expect(database['user'], 'halkyon_meditate');
expect(database['host'], 'postgres');
expect((config['sessionLogs'] as YamlMap)['persistentEnabled'], isFalse);
expect(config.keys, isNot(contains('insightsServer')));
});
});
group("the operator's library (D16)", () {
final server =
services('deploy/compose.meditate-library.yaml')['meditate-server']
as YamlMap;
test('is mounted read-only where the server reads it', () {
expect(server['volumes'], [
r'${MEDITATE_LIBRARY:?set MEDITATE_LIBRARY in deploy/.env}:/library:ro',
]);
expect(
(server['environment'] as YamlMap)['MEDITATE_LIBRARY_DIR'],
'/library',
);
});
test('is checked by a program in the image', () {
final dockerfile = read(
'apps/meditate/halkyon_meditate_server/Dockerfile',
);
expect(dockerfile, contains('--target bin/check_library.dart'));
expect(dockerfile, contains('bin/check_library'));
});
});
group('its site', () {
// The site's directives, without its comments.
final site = read('deploy/sidecars/meditate.caddy')
.split('\n')
.where((line) => !line.trim().startsWith('#'))
.join('\n');
test("sends the library's files to the web server, everything else to "
'the API server', () {
expect(site, contains('handle /library/* {'));
expect(site, contains('reverse_proxy meditate-server:8082'));
expect(site, contains('reverse_proxy meditate-server:8080'));
});
test("never passes on the calls between the instance's services", () {
expect(site, isNot(contains('/halkyon-base')));
expect(
RegExp('reverse_proxy meditate-server:8082').allMatches(site),
hasLength(1),
);
});
test('is mounted into Caddy by the compose file', () {
final caddy =
services('deploy/compose.meditate.yaml')['caddy'] as YamlMap;
expect(
caddy['volumes'],
contains(
'./sidecars/meditate.caddy:/etc/caddy/sidecars/meditate.caddy:ro',
),
);
});
});
group('the deploy and the backups', () {
test('build, dump, migrate and wait for it as for Todo', () {
expect(
read('deploy/redeploy.sh'),
contains('for app in train todo meditate; do'),
);
});
test('back its database up beside the others', () {
expect(
read('deploy/backup.sh'),
contains('for app in train todo meditate; do'),
);
});
test("have tool/check.dart replay its image's resolution", () {
expect(
dartImages,
contains('apps/meditate/halkyon_meditate_server/Dockerfile'),
);
});
});
group("every app's server names the instance's domain, not halkyon.eu's", () {
for (final (file, service, variable) in [
('compose.learn.yaml', 'server', 'LEARN_PUBLIC_HOST'),
('compose.train.yaml', 'train-server', 'TRAIN_DOMAIN'),
('compose.todo.yaml', 'todo-server', 'TODO_DOMAIN'),
('compose.meditate.yaml', 'meditate-server', 'MEDITATE_DOMAIN'),
]) {
test(service, () {
final environment =
(services('deploy/$file')[service] as YamlMap)['environment']
as YamlMap;
for (final key in [
'SERVERPOD_API_SERVER_PUBLIC_HOST',
'SERVERPOD_WEB_SERVER_PUBLIC_HOST',
]) {
expect(environment[key], startsWith('\${$variable'));
}
});
}
});
}