from __future__ import annotations import base64 import hashlib import json import tempfile import unittest from datetime import date from pathlib import Path from typing import Any, Dict, List from arr_web.app import PortalApplication, RuntimeHealth, SessionLedger from arr_web.auth import LoginCredentials from arr_web.downloads import ( ArtifactDescriptor, ControlledProjectArtifactReader, ManagedObjectArtifactReader, RoutedArtifactReader, ) from arr_web.repository import PortalDataError from arr_storage.filesystem import FilesystemObjectBackend from arr_storage.store import ManagedObjectStore PROJECT_ROOT = Path(__file__).resolve().parents[1] STATIC_ROOT = PROJECT_ROOT / "arr_web" / "static" TEST_CREDENTIALS = LoginCredentials( username="arr-test-operator", password="arr-test-password", ) class FakeRepository: def __init__(self) -> None: self.months = [ { "month_key": "2026-07", "max_arrival_date": "2026-07-26", "updated_at": "2026-07-28T12:00:00+08:00", "filename": "monthly.xlsx", "source_monthly_sha256": "a" * 64, "channel_count": 2, "row_count": 3, } ] def list_jobs( self, month_key: str, limit: int = 50, offset: int = 0, ) -> tuple[List[Dict[str, Any]], int]: return [ {"job_id": "job-1", "status": "succeeded", "filename": "ARR.XML"} ], 1 def get_job_trace(self, job_id: str) -> Dict[str, Any]: return { "trace_version": "arr-job-trace-2", "job": { "job_id": job_id, "status": "succeeded", "active": False, "current_stage": "database", }, "evidence": {"attempts": 1}, "logs": [ { "id": "run.received", "timestamp": "2026-07-29T11:00:00+00:00", "stage": "upload", "level": "success", "code": "UPLOAD_REGISTERED", "title": "上传已安全登记", "message": "任务已创建", } ], } def list_monthly_runs( self, month_key: str, limit: int = 50, offset: int = 0, ) -> tuple[List[Dict[str, Any]], int]: return [ { "report_id": 3, "version_no": 3, "status": "active", "max_arrival_date": "2026-07-26", } ], 1 def list_months(self) -> List[Dict[str, Any]]: return self.months def read_dashboard(self, month_key: str) -> Dict[str, Any]: if month_key == "2026-06": raise PortalDataError( "ANALYTICS_MONTH_NOT_FOUND", "current monthly database projection was not found", ) return { "version": "1.2", "month_key": month_key, "updated_at": "2026-07-28T12:00:00+08:00", "max_arrival_date": "2026-07-26", "source_monthly_sha256": "a" * 64, "overall": { "totals": { "rooms_sold": 3, "total_price": 1200, "room_nights": 5, "reservation_rows": 3, "room_type_count": 1, "channel_count": 2, }, "room_types": [{"room_type": "KING", "rooms_sold": 3}], }, "channels": [ { "worksheet": "COMPANY-A", "totals": {"rooms_sold": 2, "total_price": 800}, "room_types": [{"room_type": "KING", "rooms_sold": 2}], }, { "worksheet": "COMPANY-B", "totals": {"rooms_sold": 1, "total_price": 400}, "room_types": [{"room_type": "KING", "rooms_sold": 1}], }, ], } def read_channel_detail( self, month_key: str, worksheet: str, limit: int, offset: int, ) -> Dict[str, Any]: return {"month_key": month_key, "worksheet": worksheet, "rows": []} def resolve_daily_download(self, job_id: str) -> ArtifactDescriptor: return ArtifactDescriptor( "daily_xlsx", "7.26.xlsx", "private/daily.xlsx", "a" * 64, 4, "application/xlsx" ) def resolve_monthly_download(self, report_id: int) -> ArtifactDescriptor: return ArtifactDescriptor( "monthly_xlsx", "月报.xlsx", "private/monthly.xlsx", "b" * 64, 4, "application/xlsx" ) class FakeUpload: def __init__(self) -> None: self.calls: list[tuple[str, bytes]] = [] def submit(self, original_filename: str, payload: bytes) -> Dict[str, Any]: self.calls.append((original_filename, payload)) return {"job_id": "arr-job-1", "status": "queued"} class FakeMonthly: def __init__(self) -> None: self.calls: list[tuple[str, date]] = [] def generate(self, month_key: str, as_of_date: date) -> Dict[str, Any]: self.calls.append((month_key, as_of_date)) return {"status": "success", "month_key": month_key} class FakeArtifactReader: def read(self, descriptor: ArtifactDescriptor) -> bytes: return b"xlsx" def decoded(response: Any) -> Dict[str, Any]: return json.loads(response.body.decode("utf-8")) def login( app: PortalApplication, ) -> tuple[Any, Dict[str, str]]: response = app.handle( "POST", "/api/login", {"Content-Type": "application/json"}, json.dumps( { "username": TEST_CREDENTIALS.username, "password": TEST_CREDENTIALS.password, } ).encode("utf-8"), ) if response.status != 200: raise AssertionError(f"test login failed with HTTP {response.status}") payload = decoded(response) cookie = response.headers["Set-Cookie"].split(";", 1)[0] return response, { "Cookie": cookie, "X-ARR-CSRF": payload["data"]["csrf_token"], } class PortalApplicationTests(unittest.TestCase): def setUp(self) -> None: self.upload = FakeUpload() self.monthly = FakeMonthly() self.app = PortalApplication( repository=FakeRepository(), upload=self.upload, monthly=self.monthly, health=RuntimeHealth(True, True, True), sessions=SessionLedger(), credentials=TEST_CREDENTIALS, ) _, self.auth_headers = login(self.app) def test_daily_history_hides_internal_filename_when_provenance_is_missing(self) -> None: script = (STATIC_ROOT / "app.js").read_text(encoding="utf-8") self.assertIn('job.filename || "—"', script) def session_headers(self) -> Dict[str, str]: return dict(self.auth_headers) def test_session_cookie_is_secure_only_when_explicitly_enabled(self) -> None: default_cookie = login(self.app)[0].headers["Set-Cookie"] secure_app = PortalApplication( sessions=SessionLedger(), secure_cookies=True, credentials=TEST_CREDENTIALS, ) secure_cookie = login(secure_app)[0].headers["Set-Cookie"] self.assertNotIn("; Secure", default_cookie) self.assertIn("; Secure", secure_cookie) self.assertIn("; HttpOnly", secure_cookie) self.assertIn("; SameSite=Strict", secure_cookie) def test_fixed_static_routes_and_required_content(self) -> None: desktop = self.app.handle("GET", "/", self.auth_headers) mobile = self.app.handle("GET", "/h5", self.auth_headers) self.assertEqual(desktop.status, 200) self.assertEqual(mobile.status, 200) desktop_text = desktop.body.decode("utf-8") mobile_text = mobile.body.decode("utf-8") for expected in ( 'ARR Report', '
| 版本 | ', desktop_text, ) for required_id in ( 'id="process-log"', 'id="trace-live-state"', 'id="refresh-trace"', 'id="copy-trace"', 'id="jobs-prev"', 'id="jobs-next"', 'id="monthly-prev"', 'id="monthly-next"', 'id="company-prev"', 'id="company-next"', 'id="company-excel-file"', 'id="company-upload-button"', 'id="company-review-panel"', 'id="company-review-filename"', 'id="company-review-body"', 'id="company-review-activate"', 'id="company-review-discard"', 'id="company-review-delete-selected"', 'id="company-review-select-page"', 'id="company-review-confirm-dialog"', 'id="company-report-confirm-dialog"', 'id="company-report-confirm-cancel"', 'id="company-report-confirm-submit"', ): self.assertIn(required_id, desktop_text) self.assertIn("复制全部日志", desktop_text) self.assertIn('
|---|