#!/usr/bin/env python3 import base64 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_credentials_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("AUTH_USERNAME", str(ctx.exception)) def test_remote_access_accepts_valid_basic_auth_for_non_local_clients(self): handler = DummyHandler("/api/config") handler.client_address = ("192.168.1.25", 8421) encoded = base64.b64encode(b"demo:secret-pass").decode("ascii") handler.headers["Authorization"] = f"Basic {encoded}" with mock.patch.dict( os.environ, { "ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_AUTH_USERNAME": "demo", "ANI_CLI_WEB_AUTH_PASSWORD": "secret-pass", }, clear=False, ): APP.Handler.ensure_client_access(handler) def test_remote_access_accepts_valid_session_cookie_for_non_local_clients(self): handler = DummyHandler("/api/config") handler.client_address = ("192.168.1.25", 8421) session_value = http_handler.remote_access_session_value( {"auth_username": "demo", "auth_password": "secret-pass"} ) handler.headers["Cookie"] = f"ani_cli_web_session={session_value}" with mock.patch.dict( os.environ, { "ANI_CLI_WEB_ALLOW_REMOTE": "1", "ANI_CLI_WEB_AUTH_USERNAME": "demo", "ANI_CLI_WEB_AUTH_PASSWORD": "secret-pass", }, clear=False, ): APP.Handler.ensure_client_access(handler) def test_remote_browser_get_without_session_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_AUTH_USERNAME": "demo", "ANI_CLI_WEB_AUTH_PASSWORD": "secret-pass", }, clear=False, ): APP.Handler.do_GET(handler) body = handler.wfile.getvalue().decode("utf-8") self.assertEqual(handler.response_status, HTTPStatus.UNAUTHORIZED) self.assertIn('