{ config, lib, pkgs, ... }: pkgs.testers.runNixOSTest { name = "jellyfin-qbittorrent-monitor"; nodes = { server = { pkgs, config, ... }: { # Mock qBittorrent service systemd.services.mock-qbittorrent = { description = "Mock qBittorrent API server"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; ExecStart = lib.getExe ( pkgs.writers.writePython3Bin "mock-qbt" { flakeIgnore = [ "E501" ]; } '' import http.server import socketserver class MockQBittorrentHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.path == '/api/v2/transfer/speedLimitsMode': self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() response = '1' if getattr(self.server, 'speed_limits_mode', False) else '0' self.wfile.write(response.encode()) else: self.send_response(404) self.end_headers() def do_POST(self): if self.path == '/api/v2/transfer/toggleSpeedLimitsMode': self.server.speed_limits_mode = not getattr(self.server, 'speed_limits_mode', False) self.send_response(200) self.end_headers() print(f'MONITOR_TEST: Speed limits toggled to {self.server.speed_limits_mode}') else: self.send_response(404) self.end_headers() def log_message(self, format, *args): print(f'qBittorrent Mock: {format % args}') with socketserver.TCPServer(('127.0.0.1', 8080), MockQBittorrentHandler) as httpd: httpd.speed_limits_mode = False print('Mock qBittorrent server started on port 8080') httpd.serve_forever() '' ); Restart = "always"; RestartSec = "5s"; }; }; # Mock Jellyfin service with controllable streaming state systemd.services.mock-jellyfin = { description = "Mock Jellyfin API server"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; ExecStart = lib.getExe ( pkgs.writers.writePython3Bin "mock-jellyfin" { } '' import http.server import socketserver import os import json class MockJellyfinHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.path == '/Sessions': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() # Check if we should simulate streaming streaming_file = '/tmp/jellyfin_streaming' if os.path.exists(streaming_file): # Simulate external streaming session sessions = [{ 'Id': 'test-session-1', 'UserName': 'ExternalUser', 'RemoteEndPoint': '203.0.113.42', # External IP 'NowPlayingItem': { 'Name': 'Test Movie', 'Type': 'Movie' }, 'PlayState': { 'IsPaused': False } }] else: # No streaming sessions sessions = [] self.wfile.write(json.dumps(sessions).encode()) else: self.send_response(404) self.end_headers() def log_message(self, format, *args): print(f'Jellyfin Mock: {format % args}') with socketserver.TCPServer(('127.0.0.1', 8096), MockJellyfinHandler) as httpd: print('Mock Jellyfin server started on port 8096') httpd.serve_forever() '' ); Restart = "always"; RestartSec = "5s"; }; }; environment.systemPackages = with pkgs; [ curl python3 ]; networking.firewall.allowedTCPPorts = [ 8096 8080 ]; }; }; testScript = '' start_all() # Wait for services to start server.wait_for_unit("multi-user.target") server.wait_for_unit("mock-jellyfin.service") server.wait_for_unit("mock-qbittorrent.service") # Wait for services to be accessible server.wait_for_open_port(8096) # Mock Jellyfin server.wait_for_open_port(8080) # Mock qBittorrent import time time.sleep(5) # TEST 1: Verify initial state - no streaming, normal speed limits qbt_result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode") assert qbt_result.strip() == "0", "qBittorrent should start with normal speed limits" # Verify no streaming sessions initially sessions_result = server.succeed("curl -s http://localhost:8096/Sessions") print(f"Initial Jellyfin sessions: {sessions_result}") assert "[]" in sessions_result, "Should be no streaming sessions initially" # Start the monitor python_path = "${pkgs.python3.withPackages (ps: with ps; [ requests ])}/bin/python" monitor_path = "${../services/jellyfin-qbittorrent-monitor.py}" server.succeed(f""" systemd-run --unit=jellyfin-qbittorrent-monitor-test \\ --setenv=JELLYFIN_URL=http://localhost:8096 \\ --setenv=QBITTORRENT_URL=http://localhost:8080 \\ --setenv=CHECK_INTERVAL=2 \\ {python_path} {monitor_path} """) # Wait for monitor to start time.sleep(3) # TEST 2: Verify monitor runs and keeps normal limits with no streaming qbt_result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode") assert qbt_result.strip() == "0", "Should maintain normal speed limits with no streaming" # TEST 3: Simulate external streaming and verify throttling print("\\nSimulating external streaming session...") server.succeed("touch /tmp/jellyfin_streaming") # Signal mock to return streaming session # Wait for monitor to detect streaming and apply throttling (includes hysteresis delay) time.sleep(15) qbt_result_streaming = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode") # Check if throttling was enabled assert qbt_result_streaming.strip() == "1", "External streaming should enable qBittorrent throttling" # TEST 4: Stop streaming and verify throttling is removed print("\\nStopping streaming session...") server.succeed("rm -f /tmp/jellyfin_streaming") # Signal mock to return no sessions # Wait for monitor to detect no streaming and remove throttling (includes hysteresis delay) time.sleep(65) qbt_result_no_streaming = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode") assert qbt_result_no_streaming.strip() == "0", "No streaming should disable qBittorrent throttling" ''; }