142 lines
4.8 KiB
Dart
142 lines
4.8 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
|
||
|
|
/// halkyon.li's first `.env` (ADR 0039; the developer's choices of 8 Nov 2026):
|
||
|
|
/// every secret made fresh on the host, halkyon.li's settings, and never an existing
|
||
|
|
/// file overwritten, as the session peppers must never change once people sign in.
|
||
|
|
void main() {
|
||
|
|
const script = 'deploy/host/halkyon-li-env.sh';
|
||
|
|
|
||
|
|
late Directory folder;
|
||
|
|
setUp(() => folder = Directory.systemTemp.createTempSync('halkyon-li-env'));
|
||
|
|
tearDown(() => folder.deleteSync(recursive: true));
|
||
|
|
|
||
|
|
ProcessResult make(String target) => Process.runSync('sh', [script, target]);
|
||
|
|
|
||
|
|
Map<String, String> settings(String text) => {
|
||
|
|
for (final line in text.split('\n'))
|
||
|
|
if (line.isNotEmpty && !line.startsWith('#'))
|
||
|
|
line.substring(0, line.indexOf('=')): line.substring(
|
||
|
|
line.indexOf('=') + 1,
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
group('a new .env', () {
|
||
|
|
late Map<String, String> env;
|
||
|
|
late File file;
|
||
|
|
|
||
|
|
setUp(() {
|
||
|
|
file = File('${folder.path}/.env');
|
||
|
|
final result = make(file.path);
|
||
|
|
expect(result.exitCode, 0, reason: '${result.stderr}');
|
||
|
|
env = settings(file.readAsStringSync());
|
||
|
|
});
|
||
|
|
|
||
|
|
test('is readable by its owner alone', () {
|
||
|
|
expect(file.statSync().modeString(), 'rw-------');
|
||
|
|
});
|
||
|
|
|
||
|
|
test("makes every secret fresh, each of 32 bytes, none another's", () {
|
||
|
|
const secrets = [
|
||
|
|
'INSTANCE_SECRET',
|
||
|
|
'LEARN_POSTGRES_PASSWORD',
|
||
|
|
'LEARN_SESSION_KEY_HASH_PEPPER',
|
||
|
|
'SIGN_IN_DECOY_KEY',
|
||
|
|
'TRAIN_POSTGRES_PASSWORD',
|
||
|
|
'TRAIN_SESSION_KEY_HASH_PEPPER',
|
||
|
|
'MAP_KEY_SECRET',
|
||
|
|
'TODO_POSTGRES_PASSWORD',
|
||
|
|
'TODO_SESSION_KEY_HASH_PEPPER',
|
||
|
|
'MEDITATE_POSTGRES_PASSWORD',
|
||
|
|
'MEDITATE_SESSION_KEY_HASH_PEPPER',
|
||
|
|
];
|
||
|
|
for (final name in secrets) {
|
||
|
|
expect(env[name], matches(RegExp(r'^[0-9a-f]{64}$')), reason: name);
|
||
|
|
}
|
||
|
|
expect(
|
||
|
|
secrets.map((name) => env[name]).toSet(),
|
||
|
|
hasLength(secrets.length),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('differs from the next one made', () {
|
||
|
|
final other = File('${folder.path}/other.env');
|
||
|
|
make(other.path);
|
||
|
|
expect(
|
||
|
|
settings(other.readAsStringSync())['INSTANCE_SECRET'],
|
||
|
|
isNot(env['INSTANCE_SECRET']),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("serves halkyon.li's names, handles on halkyon.li itself (D7)", () {
|
||
|
|
expect(env['LEARN_DOMAIN'], 'learn.halkyon.li');
|
||
|
|
expect(env['LEARN_PUBLIC_HOST'], 'learn.halkyon.li');
|
||
|
|
expect(env['ACCOUNT_DOMAIN'], 'halkyon.li');
|
||
|
|
expect(env['TRAIN_DOMAIN'], 'train.halkyon.li');
|
||
|
|
expect(env['TODO_DOMAIN'], 'todo.halkyon.li');
|
||
|
|
expect(env['MEDITATE_DOMAIN'], 'meditate.halkyon.li');
|
||
|
|
expect(env['JELLYFIN_DOMAIN'], 'jellyfin.halkyon.li');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('lets people in through SimpleLogin alone, once an operator '
|
||
|
|
'approves them, and mails nothing', () {
|
||
|
|
expect(env['SIGN_IN_WAYS'], 'simplelogin');
|
||
|
|
expect(env['SIGN_UP'], 'approval');
|
||
|
|
expect(env['SIMPLELOGIN_CLIENT_ID'], isEmpty);
|
||
|
|
expect(env['SIMPLELOGIN_CLIENT_SECRET'], isEmpty);
|
||
|
|
for (final name in [
|
||
|
|
'EMAIL_SIGN_IN_PEPPER',
|
||
|
|
'EMAIL_SMTP_HOST',
|
||
|
|
'TOTP_KEY',
|
||
|
|
'RECOVERY_CODE_KEY',
|
||
|
|
]) {
|
||
|
|
expect(env[name] ?? '', isEmpty, reason: name);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('runs everything chosen, behind the forward, each file there', () {
|
||
|
|
final files = env['COMPOSE_FILE']!.split(':');
|
||
|
|
expect(files, [
|
||
|
|
'compose.learn.yaml',
|
||
|
|
'compose.jellyfin.yaml',
|
||
|
|
'compose.languagetool.yaml',
|
||
|
|
'compose.train.yaml',
|
||
|
|
'compose.maps.yaml',
|
||
|
|
'compose.search.yaml',
|
||
|
|
'compose.todo.yaml',
|
||
|
|
'compose.todo-search.yaml',
|
||
|
|
'compose.meditate.yaml',
|
||
|
|
'compose.forwarded.yaml',
|
||
|
|
]);
|
||
|
|
for (final name in files) {
|
||
|
|
expect(File('deploy/$name').existsSync(), isTrue, reason: name);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("keeps Train's map and routes to southern Bavaria, its search to "
|
||
|
|
'Germany, in memory beside the rest', () {
|
||
|
|
expect(env['MAP_REGION'], '10,47.2,13.9,49');
|
||
|
|
expect(env['BROUTER_REGION'], '10,45,15,50');
|
||
|
|
expect(env['PHOTON_REGION'], 'europe/germany');
|
||
|
|
expect(env['PHOTON_HEAP'], '2g');
|
||
|
|
expect(env['PHOTON_MEMORY'], '3g');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('says Germany for the data and the backups, and reads the library '
|
||
|
|
'from the HDD', () {
|
||
|
|
expect(env['DATA_LOCATION'], 'DE');
|
||
|
|
expect(env['BACKUP_LOCATION'], 'DE');
|
||
|
|
expect(env['JELLYFIN_MEDIA'], '/data/media');
|
||
|
|
expect(env.containsKey('MEDITATE_LIBRARY'), isFalse);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('never overwrites an existing .env, whose peppers must stay', () {
|
||
|
|
final file = File('${folder.path}/.env')..writeAsStringSync('KEPT=1\n');
|
||
|
|
final result = make(file.path);
|
||
|
|
expect(result.exitCode, isNot(0));
|
||
|
|
expect(result.stderr, contains('exists'));
|
||
|
|
expect(file.readAsStringSync(), 'KEPT=1\n');
|
||
|
|
});
|
||
|
|
}
|