import 'dart:io'; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; /// Jellyfin at home, published by the instance through WireGuard (ADR 0032): /// the tunnel reaches Jellyfin from Caddy alone, and nothing else of the /// instance's server; nothing CI's checks would ever run, so held here. void main() { YamlMap compose(String path) => loadYaml(File(path).readAsStringSync()) as YamlMap; group('the instance publishes it (D7, D9)', () { final home = compose('deploy/compose.jellyfin-home.yaml'); final services = home['services'] as YamlMap; test('runs no Jellyfin of its own', () { expect(services.keys, isNot(contains('jellyfin'))); }); test('has Caddy proxy to the home end over the tunnel', () { final caddy = services['caddy'] as YamlMap; final environment = caddy['environment'] as YamlMap; expect(environment['JELLYFIN_UPSTREAM'], '10.61.96.2:8096'); expect( caddy['volumes'], contains( './sidecars/jellyfin.caddy:/etc/caddy/sidecars/jellyfin.caddy:ro', ), ); }); test('never sets JELLYFIN_URL, so the instance suggests it only while it ' 'answers', () { final server = services['server'] as YamlMap?; final environment = server?['environment'] as YamlMap?; expect(environment?.keys ?? const [], isNot(contains('JELLYFIN_URL'))); }); test('has the server look at Jellyfin through Caddy on edge', () { final server = services['server'] as YamlMap; expect( server['extra_hosts'], contains( r'${JELLYFIN_DOMAIN:?set JELLYFIN_DOMAIN in deploy/.env}:' '172.30.80.2', ), ); }); }); group("the sidecar's site", () { final site = File('deploy/sidecars/jellyfin.caddy').readAsStringSync(); test('proxies to the sidecar unless told otherwise', () { expect( site, contains(r'reverse_proxy {$JELLYFIN_UPSTREAM:jellyfin:8096}'), ); }); test('still refuses changes to users and the setup wizard', () { expect(site, contains('respond @userChanges 403')); expect(site, contains('respond /Startup/* 403')); }); }); group("the server's end of the tunnel (D4, D5)", () { final config = File('deploy/home-jellyfin/server/wg-jellyfin.conf') .readAsStringSync(); final rules = File('deploy/home-jellyfin/server/wg-jellyfin-rules.sh') .readAsStringSync(); test('lets the home end send from its own address alone', () { expect( RegExp( r'^AllowedIPs = (.+)$', multiLine: true, ).allMatches(config).map((match) => match[1]), ['10.61.96.2/32'], ); }); test('holds no endpoint for the home end', () { expect(config, isNot(contains('Endpoint'))); }); test('sets its rules as it comes up and takes them away as it goes', () { expect( config, contains('PostUp = /etc/wireguard/wg-jellyfin-rules.sh up'), ); expect( config, contains('PreDown = /etc/wireguard/wg-jellyfin-rules.sh down'), ); }); test('forwards only Caddy to Jellyfin into the tunnel, and only replies ' 'out of it', () { expect( rules, contains( r'-o "$interface" -s 172.30.80.2 -d 10.61.96.2 -p tcp --dport 8096 ' '-j RETURN', ), ); expect( rules, contains( r'-i "$interface" -m conntrack --ctstate ESTABLISHED,RELATED ' '-j RETURN', ), ); expect(rules, contains(r'iptables -A "$forward" -j DROP')); }); test('accepts no new connection from the tunnel on the host', () { expect(rules, contains(r'iptables -A "$input" -j DROP')); expect(rules, contains(r'iptables -I INPUT -i "$interface" -j "$input"')); }); }); test("the CI runner can't reach the tunnel (D6)", () { final unit = File('deploy/runner/forgejo-runner.service') .readAsStringSync(); final denied = RegExp( r'^IPAddressDeny=(.+)$', multiLine: true, ).firstMatch(unit)![1]!.split(' '); expect(denied, contains('10.61.96.0/30')); }); group('the home end (D2)', () { final home = compose('deploy/home-jellyfin/compose.yaml'); final services = home['services'] as YamlMap; test("has Jellyfin inside the tunnel's network namespace", () { final jellyfin = services['jellyfin'] as YamlMap; expect(jellyfin['network_mode'], 'service:wireguard'); expect(jellyfin.keys, isNot(contains('ports'))); }); test("publishes Jellyfin's port on this machine's loopback alone", () { final wireguard = services['wireguard'] as YamlMap; expect(wireguard['ports'], ['127.0.0.1:8096:8096']); }); test('tells Jellyfin its public address, never a home one', () { final jellyfin = services['jellyfin'] as YamlMap; final environment = jellyfin['environment'] as YamlMap; expect( environment['JELLYFIN_PublishedServerUrl'], 'https://jellyfin.halkyon.eu', ); }); }); }