#!/usr/bin/env python3 import importlib.util import io import os import sys import tempfile import threading import unittest from http import HTTPStatus from pathlib import Path from unittest import mock ROOT = Path(__file__).resolve().parent MODULE_PATH = ROOT / "app.py" TEMP_STATE = tempfile.TemporaryDirectory() if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import queue_jobs import http_handler os.environ.setdefault("ANI_CLI_WEB_DISABLE_WORKER", "1") os.environ["ANI_CLI_WEB_STATE_ROOT"] = TEMP_STATE.name SPEC = importlib.util.spec_from_file_location("ani_cli_web_app", MODULE_PATH) APP = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None SPEC.loader.exec_module(APP) IMPORT_RUNTIME_WAS_DEFERRED = ( APP.CONFIG is None and APP.WATCHLIST is None and APP.DOWNLOAD_QUEUE is None and APP.WATCHLIST_REFRESH is None ) APP.initialize_runtime(start_workers=False) APP.reset_runtime(wait=True) APP.initialize_runtime(start_workers=False) unittest.addModuleCleanup(lambda: APP.reset_runtime(wait=True)) class DummyHandler: def __init__(self, path, body=b"{}", content_length=None): self.path = path self.client_address = ("127.0.0.1", 8421) self.handler_context = APP.Handler.handler_context self.headers = {} if content_length is not None: self.headers["Content-Length"] = str(content_length) self.rfile = io.BytesIO(body) self.json_payload = None self.json_status = None self.error_status = None self.error_message = None self.file_path = None self.response_status = None self.response_headers = [] self.wfile = io.BytesIO() def ensure_client_access(self): return APP.Handler.ensure_client_access(self) def body_json(self): return APP.Handler.body_json(self) def json(self, payload, status=HTTPStatus.OK): self.json_payload = payload self.json_status = status def error(self, status, message): self.error_status = status self.error_message = message def file(self, path): self.file_path = path def html(self, _content): raise AssertionError("HTML should not be served in this test") def exception(self, exc): return APP.Handler.exception(self, exc) def send_response(self, status): self.response_status = status def send_header(self, key, value): self.response_headers.append((key, value)) def end_headers(self): return None class NetworkGuardTests(unittest.TestCase): def test_runtime_bootstrap_is_deferred_until_initialize(self): self.assertTrue(IMPORT_RUNTIME_WAS_DEFERRED) def test_client_address_is_local_accepts_loopback(self): self.assertTrue(APP.client_address_is_local("127.0.0.1")) self.assertTrue(APP.client_address_is_local("::1")) self.assertTrue(APP.client_address_is_local("localhost")) def test_client_address_is_local_rejects_non_loopback(self): self.assertFalse(APP.client_address_is_local("192.168.1.25")) self.assertFalse(APP.client_address_is_local("10.0.0.8")) def test_remote_access_requires_token_for_non_local_clients(self): handler = DummyHandler("/api/config") handler.client_address = ("192.168.1.25", 8421) with mock.patch.dict(os.environ, {"ANI_CLI_WEB_ALLOW_REMOTE": "1"}, clear=False): with self.assertRaises(PermissionError) as ctx: APP.Handler.ensure_client_access(handler) self.assertIn("REMOTE_TOKEN", str(ctx.exception)) def test_remote_access_accepts_valid_token_for_non_local_clients(self): handler = DummyHandler("/api/config") handler.client_address = ("192.168.1.25", 8421) handler.headers["Authorization"] = "Bearer secret-token" with mock.patch.dict( os.environ, {"ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_REMOTE_TOKEN": "secret-token"}, clear=False, ): APP.Handler.ensure_client_access(handler) def test_remote_access_accepts_valid_cookie_token_for_non_local_clients(self): handler = DummyHandler("/api/config") handler.client_address = ("192.168.1.25", 8421) handler.headers["Cookie"] = "ani_cli_web_token=secret-token" with mock.patch.dict( os.environ, {"ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_REMOTE_TOKEN": "secret-token"}, clear=False, ): APP.Handler.ensure_client_access(handler) def test_remote_access_accepts_valid_query_token_for_non_local_clients(self): handler = DummyHandler("/api/config?token=secret-token") handler.client_address = ("192.168.1.25", 8421) with mock.patch.dict( os.environ, {"ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_REMOTE_TOKEN": "secret-token"}, clear=False, ): APP.Handler.ensure_client_access(handler) def test_remote_get_bootstraps_cookie_and_redirects_when_query_token_valid(self): handler = DummyHandler("/?token=secret-token") handler.client_address = ("192.168.1.25", 8421) with mock.patch.dict( os.environ, {"ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_REMOTE_TOKEN": "secret-token"}, clear=False, ): APP.Handler.do_GET(handler) headers = dict(handler.response_headers) self.assertEqual(handler.response_status, HTTPStatus.SEE_OTHER) self.assertEqual(headers.get("Location"), "/") self.assertIn("ani_cli_web_token=secret-token", headers.get("Set-Cookie", "")) self.assertIn("Max-Age=", headers.get("Set-Cookie", "")) def test_remote_browser_get_without_token_renders_login_page(self): handler = DummyHandler("/") handler.client_address = ("192.168.1.25", 8421) handler.command = "GET" with mock.patch.dict( os.environ, {"ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_REMOTE_TOKEN": "secret-token"}, clear=False, ): APP.Handler.do_GET(handler) body = handler.wfile.getvalue().decode("utf-8") self.assertEqual(handler.response_status, HTTPStatus.UNAUTHORIZED) self.assertIn('