from __future__ import annotations import base64 import hashlib import json import re 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_history_month_counts(self) -> List[Dict[str, Any]]: return [ { "month_key": "2026-07", "daily_count": 3, "monthly_count": 2, } ] 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", "min_arrival_date": "2026-07-01", "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 FakeDailyPriceReviews: case_id = "dailyreview-0123456789abcdef0123456789abcdef" def __init__(self) -> None: self.read_calls: list[tuple[str, int, int]] = [] self.update_calls: list[tuple[str, int, str, int, str, str]] = [] self.finalize_calls: list[tuple[str, str, int, str]] = [] self.cancel_calls: list[tuple[str, str, int, str]] = [] def payload(self) -> Dict[str, Any]: return { "case_id": self.case_id, "case_status": "open", "revision": 3, "business_date": "2026-07-27", "completed_items": 1, "total_items": 2, "items": [ { "item_id": 9, "company_key": "QBD", "rate_code": "GRPA1", "effective_rate_amount": 1800, "candidate_prices": [{"effective_rate_amount": 900, "real_price": 1800}], "affected_records": 2, "affected_rooms": 3, "affected_room_nights": 3, "real_price": "0", "revision": 3, } ], "pagination": {"limit": 50, "offset": 0, "total": 2, "has_next": False}, } def get_price_review(self, job_id: str, limit: int, offset: int) -> Dict[str, Any]: self.read_calls.append((job_id, limit, offset)) return self.payload() def update_price_review_item( self, job_id: str, item_id: int, case_id: str, revision: int, real_price: str, actor_username: str, ) -> Dict[str, Any]: self.update_calls.append((job_id, item_id, case_id, revision, real_price, actor_username)) return self.payload() def finalize_price_review( self, job_id: str, case_id: str, revision: int, actor_username: str ) -> Dict[str, Any]: self.finalize_calls.append((job_id, case_id, revision, actor_username)) return {"job_id": job_id, "status": "needs_review", "review_case_id": case_id} def cancel_price_review( self, job_id: str, case_id: str, revision: int, actor_username: str ) -> Dict[str, Any]: self.cancel_calls.append((job_id, case_id, revision, actor_username)) return {"job_id": job_id, "status": "cancelled", "review_case_id": case_id} 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.price_reviews = FakeDailyPriceReviews() self.monthly = FakeMonthly() self.app = PortalApplication( repository=FakeRepository(), upload=self.upload, price_reviews=self.price_reviews, 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', '

Daily Report

', "ARRIVAL DATE", "NO. OF ROOM", "月报", "月报处理", "渠道BI", "公司渠道明细", "各公司本月销售情况", "渠道 × 房型售出房数", "任务日志", "arr-task.log", "$ arr trace", ): self.assertIn(expected, desktop_text) for removed in ( "标准月报", "VERSION HISTORY", "标准月报版本记录", "公司渠道明细按 C/O(离店日)所属期间分页", ): self.assertNotIn(removed, desktop_text) self.assertNotIn( '\n ', 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="daily-price-review-panel"', 'id="daily-price-review-body"', 'id="daily-price-review-finalize"', 'id="daily-price-review-cancel"', 'id="daily-price-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('
ARR Report', mobile_text)
        self.assertNotIn("", mobile_text)
        self.assertIn("各公司本月销售情况", mobile_text)
        self.assertIn("泰国曼谷(UTC+7)", mobile_text)
        self.assertNotIn('id="generate-monthly"', desktop_text)
        self.assertNotIn('id="monthly-as-of"', desktop_text)
        self.assertNotIn('id="monthly-month"', desktop_text)
        self.assertNotIn('id="refresh-monthly"', desktop_text)
        self.assertNotIn('$("#generate-monthly")', script)
        self.assertNotIn('$("#refresh-monthly")', script)
        self.assertIn("const MONTHLY_POLL_INTERVAL = 4000", script)
        self.assertIn("const HISTORY_PAGE_SIZE = 50", script)
        self.assertIn("function renderPagination", script)
        self.assertIn("function changeHistoryPage", script)
        self.assertIn("page.total > previousTotal", script)
        self.assertIn("state.monthlyOffset = 0", script)
        self.assertNotIn("limit=100", script)
        self.assertIn("function scheduleMonthlyPoll", script)
        self.assertIn("loadMonthly(false, true)", script)
        self.assertIn("自动更新重试中", script)
        self.assertIn('api("/api/company-reports/source"', script)
        self.assertIn("handleCompanySourceUpload", script)
        self.assertNotIn("company-chip-list", desktop_text)
        self.assertIn(
            '

固定生成:LianTai、QBD、DY-AI-Easy-KB、FengRun、HanaTour

', desktop_text, ) self.assertNotIn('class="company-scope"', desktop_text) self.assertNotIn("固定生成公司", desktop_text) self.assertIn('title="重新读取服务状态、Excel 来源和生成记录"', desktop_text) self.assertIn('title="选择 C/O 所属的生成月份"', desktop_text) self.assertIn('aria-label="生成月份"', desktop_text) self.assertIn('id="company-history-month"', desktop_text) self.assertNotIn("生成截至", desktop_text) self.assertEqual(desktop_text.count('生成'), 3) self.assertIn("生成时间", desktop_text) self.assertNotIn('data-label="版本"', script) self.assertIn("function uniqueCompanyProblems", script) self.assertIn("companyProblemsForResult(result)", script) self.assertIn("job.finished_at || job.created_at", script) for period_label in ("C/O:01-10", "C/O:11-20", "C/O:21-30"): self.assertIn(f">{period_label}", desktop_text) self.assertIn("function companyPeriodDisplayLabel(reportMonth, period)", script) self.assertIn('return monthEnd ? `C/O:21-${monthEnd}` : "C/O:21-30";', script) self.assertIn("companyPeriodDisplayLabel(reportMonth, period)", script) for removed_period_name in ("第一期", "第二期", "第三期"): self.assertNotIn(removed_period_name, desktop_text) self.assertNotIn('class="company-generator-section"', desktop_text) self.assertNotIn('class="company-source-section"', desktop_text) self.assertLess( desktop_text.index('id="company-report-month"'), desktop_text.index('id="company-excel-file"'), ) def test_all_public_assets_exclude_removed_copy(self) -> None: content = "\n".join( path.read_text(encoding="utf-8") for path in STATIC_ROOT.iterdir() if path.suffix in {".html", ".css", ".js"} ).lower() for removed in ( "codex", "本地报表处理", "渠道工作表", "所有渠道工作表", "room mix", "channel mix", "房型售卖排行", "下载后更新月报", "源记录", "白名单删除", "重复删除", "最终输出", ): self.assertNotIn(removed, content) self.assertIn("正在处理arr.xml文件", content) def test_read_apis_return_wrapped_database_data(self) -> None: jobs = decoded( self.app.handle("GET", "/api/jobs?month=2026-07", self.auth_headers) ) trace = decoded( self.app.handle("GET", "/api/jobs/job-1/trace", self.auth_headers) ) monthly = decoded( self.app.handle( "GET", "/api/monthly-runs?month=2026-07", self.auth_headers, ) ) history_months = decoded( self.app.handle("GET", "/api/history-months", self.auth_headers) ) analytics = decoded( self.app.handle( "GET", "/api/analytics?month=2026-07", self.auth_headers, ) ) h5_months = decoded( self.app.handle("GET", "/api/h5/months", self.auth_headers) ) compatible_analytics = decoded( self.app.handle( "GET", "/api/monthly/2026-07/analytics", self.auth_headers, ) ) self.assertEqual(jobs["data"][0]["job_id"], "job-1") self.assertEqual( history_months["data"], [ { "month_key": "2026-07", "daily_count": 3, "monthly_count": 2, "company_count": 0, } ], ) self.assertEqual( jobs["pagination"], { "total": 1, "limit": 50, "offset": 0, "has_previous": False, "has_next": False, }, ) self.assertEqual(trace["data"]["job"]["job_id"], "job-1") self.assertEqual(trace["data"]["logs"][0]["code"], "UPLOAD_REGISTERED") self.assertEqual(monthly["data"][0]["version_no"], 3) self.assertEqual(monthly["data"][0]["max_arrival_date"], "2026-07-26") self.assertEqual(monthly["pagination"]["total"], 1) self.assertEqual(monthly["pagination"]["limit"], 50) self.assertEqual(analytics["data"]["min_arrival_date"], "2026-07-01") self.assertEqual(analytics["data"]["channels"][0]["worksheet"], "COMPANY-A") self.assertEqual( h5_months, { "months": [ { "month_key": "2026-07", "updated_at": "2026-07-28T12:00:00+08:00", "max_arrival_date": "2026-07-26", } ] }, ) self.assertNotIn("ok", compatible_analytics) self.assertEqual(compatible_analytics["version"], "1.2") self.assertEqual( compatible_analytics["channels"][0]["worksheet"], "COMPANY-A", ) def test_anonymous_h5_exposes_only_sanitized_read_only_dashboard(self) -> None: mobile = self.app.handle("GET", "/h5", {}) mobile_css = self.app.handle("GET", "/assets/h5.css", {}) mobile_js = self.app.handle("GET", "/assets/h5.js", {}) months = decoded(self.app.handle("GET", "/api/public/h5/months", {})) analytics_response = self.app.handle( "GET", "/api/public/h5/analytics?month=2026-07", {}, ) analytics = decoded(analytics_response) protected_generic = self.app.handle( "GET", "/api/analytics?month=2026-07", {}, ) protected_legacy = self.app.handle("GET", "/api/h5/months", {}) self.assertEqual(mobile.status, 200) self.assertEqual(mobile_css.status, 200) self.assertEqual(mobile_js.status, 200) self.assertEqual(months["data"][0]["month_key"], "2026-07") self.assertEqual(analytics_response.headers["Cache-Control"], "no-store") self.assertEqual(analytics["data"]["month_key"], "2026-07") self.assertIn("channels", analytics["data"]) self.assertNotIn("source_monthly_sha256", analytics["data"]) self.assertNotIn("filename", months["data"][0]) self.assertEqual(protected_generic.status, 401) self.assertEqual(protected_legacy.status, 401) def test_health_exposes_only_programmatic_processing_readiness(self) -> None: payload = decoded( self.app.handle("GET", "/api/health", self.auth_headers) )["data"] self.assertTrue(payload["database_ready"]) self.assertTrue(payload["processing_ready"]) self.assertNotIn("agent_writeback_ready", payload) def test_legacy_h5_analytics_route_preserves_status_contract(self) -> None: invalid = self.app.handle( "GET", "/api/monthly/2026-13/analytics", self.auth_headers, ) missing = self.app.handle( "GET", "/api/monthly/2026-06/analytics", self.auth_headers, ) self.assertEqual(invalid.status, 422) self.assertEqual(decoded(invalid)["error"]["code"], "MONTH_KEY_INVALID") self.assertEqual(missing.status, 404) self.assertEqual(decoded(missing)["error"]["code"], "MONTHLY_NOT_FOUND") def test_upload_requires_csrf_and_accepts_raw_xml_only(self) -> None: encoded = base64.urlsafe_b64encode("到店明细.XML".encode("utf-8")).decode("ascii").rstrip("=") no_csrf = self.app.handle( "POST", "/api/jobs", { "Cookie": self.auth_headers["Cookie"], "X-ARR-Filename-B64": encoded, }, b"", ) self.assertEqual(no_csrf.status, 403) response = self.app.handle( "POST", "/api/jobs", {**self.session_headers(), "X-ARR-Filename-B64": encoded}, b"\xef\xbb\xbf ", ) self.assertEqual(response.status, 202) self.assertEqual(self.upload.calls, [("到店明细.XML", b"\xef\xbb\xbf ")]) def test_upload_rejects_non_xml_bytes_before_coordinator(self) -> None: encoded = base64.urlsafe_b64encode(b"report.xml").decode("ascii").rstrip("=") response = self.app.handle( "POST", "/api/jobs", {**self.session_headers(), "X-ARR-Filename-B64": encoded}, b"not xml", ) self.assertEqual(response.status, 400) self.assertEqual(decoded(response)["error"]["code"], "UPLOAD_CONTENT_INVALID") self.assertEqual(self.upload.calls, []) def test_price_review_routes_are_authenticated_csrf_protected_and_strict(self) -> None: unauthenticated = self.app.handle("GET", "/api/jobs/job-1/review?limit=5&offset=2", {}) self.assertEqual(unauthenticated.status, 401) review = self.app.handle( "GET", "/api/jobs/job-1/review?limit=5&offset=2", self.auth_headers ) self.assertEqual(review.status, 200) payload = decoded(review)["data"] self.assertEqual(payload["case_id"], FakeDailyPriceReviews.case_id) self.assertEqual(self.price_reviews.read_calls, [("job-1", 5, 2)]) rendered = json.dumps(payload, ensure_ascii=False) for private_value in ("SYNTHETIC GUEST", "RES_COMMENT", "TRACE_TEXT"): self.assertNotIn(private_value, rendered) missing_csrf = self.app.handle( "PATCH", "/api/jobs/job-1/review/items/9", {"Cookie": self.auth_headers["Cookie"], "Content-Type": "application/json"}, json.dumps({"case_id": FakeDailyPriceReviews.case_id, "revision": 3, "real_price": "0"}).encode(), ) self.assertEqual(missing_csrf.status, 403) malformed = self.app.handle( "PATCH", "/api/jobs/job-1/review/items/9", {**self.session_headers(), "Content-Type": "application/json"}, json.dumps({"case_id": FakeDailyPriceReviews.case_id, "revision": 3, "real_price": "0.00"}).encode(), ) self.assertEqual(malformed.status, 400) self.assertEqual(decoded(malformed)["error"]["code"], "REVIEW_REQUEST_INVALID") updated = self.app.handle( "PATCH", "/api/jobs/job-1/review/items/9", {**self.session_headers(), "Content-Type": "application/json"}, json.dumps({"case_id": FakeDailyPriceReviews.case_id, "revision": 3, "real_price": "0"}).encode(), ) self.assertEqual(updated.status, 200) self.assertEqual( self.price_reviews.update_calls, [("job-1", 9, FakeDailyPriceReviews.case_id, 3, "0", TEST_CREDENTIALS.username)], ) finalized = self.app.handle( "POST", "/api/jobs/job-1/review/finalize", {**self.session_headers(), "Content-Type": "application/json"}, json.dumps({"case_id": FakeDailyPriceReviews.case_id, "revision": 3}).encode(), ) cancelled = self.app.handle( "POST", "/api/jobs/job-1/review/cancel", {**self.session_headers(), "Content-Type": "application/json"}, json.dumps({"case_id": FakeDailyPriceReviews.case_id, "revision": 3}).encode(), ) self.assertEqual(finalized.status, 200) self.assertEqual(cancelled.status, 200) self.assertEqual( self.price_reviews.finalize_calls, [("job-1", FakeDailyPriceReviews.case_id, 3, TEST_CREDENTIALS.username)], ) self.assertEqual( self.price_reviews.cancel_calls, [("job-1", FakeDailyPriceReviews.case_id, 3, TEST_CREDENTIALS.username)], ) def test_monthly_generation_is_csrf_protected_and_typed(self) -> None: response = self.app.handle( "POST", "/api/monthly-runs", {**self.session_headers(), "Content-Type": "application/json"}, b'{"month":"2026-07","as_of_date":"2026-07-26"}', ) self.assertEqual(response.status, 202) self.assertEqual(self.monthly.calls, [("2026-07", date(2026, 7, 26))]) def test_agent_result_callback_is_not_exposed_in_programmatic_runtime(self) -> None: response = self.app.handle( "POST", "/api/integrations/super-agent/results", { **self.session_headers(), "Content-Type": "application/json; charset=utf-8", }, b"{}", ) self.assertEqual(response.status, 404) self.assertEqual(decoded(response)["error"]["code"], "ROUTE_NOT_FOUND") def test_download_uses_internal_descriptor_without_exposing_storage_key(self) -> None: app = PortalApplication( repository=FakeRepository(), artifact_reader=FakeArtifactReader(), health=RuntimeHealth(True, True, True, True), credentials=TEST_CREDENTIALS, ) _, headers = login(app) response = app.handle( "GET", "/api/download/monthly?report_id=3", headers, ) self.assertEqual(response.status, 200) self.assertEqual(response.body, b"xlsx") self.assertIn("filename*=UTF-8''", response.headers["Content-Disposition"]) self.assertEqual(response.headers["Cache-Control"], "private, no-store") self.assertNotIn("private/monthly.xlsx", str(response.headers)) def test_query_validation_fails_without_repository_call(self) -> None: response = self.app.handle( "GET", "/api/jobs?month=2026-13", self.auth_headers, ) self.assertEqual(response.status, 400) self.assertEqual(decoded(response)["error"]["code"], "MONTH_INVALID") invalid_job = self.app.handle( "GET", "/api/jobs/not%2Fsafe/trace", self.auth_headers, ) self.assertEqual(invalid_job.status, 400) self.assertEqual(decoded(invalid_job)["error"]["code"], "JOB_ID_INVALID") invalid_offset = self.app.handle( "GET", "/api/monthly-runs?month=2026-07&offset=-1", self.auth_headers, ) self.assertEqual(invalid_offset.status, 400) self.assertEqual(decoded(invalid_offset)["error"]["code"], "QUERY_INVALID") def test_history_routes_forward_limit_and_offset_with_exact_totals(self) -> None: class PagingRepository(FakeRepository): def __init__(self) -> None: super().__init__() self.job_request: tuple[str, int, int] | None = None self.monthly_request: tuple[str, int, int] | None = None def list_jobs( self, month_key: str, limit: int = 50, offset: int = 0, ) -> tuple[List[Dict[str, Any]], int]: self.job_request = (month_key, limit, offset) return [{"job_id": "job-51"}], 73 def list_monthly_runs( self, month_key: str, limit: int = 50, offset: int = 0, ) -> tuple[List[Dict[str, Any]], int]: self.monthly_request = (month_key, limit, offset) return [{"report_id": 51}], 101 repository = PagingRepository() app = PortalApplication( repository=repository, credentials=TEST_CREDENTIALS, ) _, headers = login(app) jobs = decoded( app.handle( "GET", "/api/jobs?month=2026-07&limit=50&offset=50", headers, ) ) monthly = decoded( app.handle( "GET", "/api/monthly-runs?month=2026-07&limit=50&offset=50", headers, ) ) self.assertEqual(repository.job_request, ("2026-07", 50, 50)) self.assertEqual(repository.monthly_request, ("2026-07", 50, 50)) self.assertEqual(jobs["pagination"]["total"], 73) self.assertTrue(jobs["pagination"]["has_previous"]) self.assertTrue(jobs["pagination"]["has_next"]) self.assertEqual(monthly["pagination"]["total"], 101) self.assertTrue(monthly["pagination"]["has_previous"]) self.assertTrue(monthly["pagination"]["has_next"]) class UnavailableDataTests(unittest.TestCase): def test_repository_failures_are_privacy_safe_503(self) -> None: class Broken(FakeRepository): def list_jobs( self, month_key: str, limit: int = 50, offset: int = 0, ) -> tuple[List[Dict[str, Any]], int]: raise PortalDataError("DATABASE_UNAVAILABLE", "数据库连接失败") app = PortalApplication( repository=Broken(), credentials=TEST_CREDENTIALS, ) _, headers = login(app) response = app.handle( "GET", "/api/jobs?month=2026-07", headers, ) self.assertEqual(response.status, 503) payload = decoded(response) self.assertEqual(payload["error"]["message"], "数据库连接失败") self.assertNotIn("password", response.body.decode("utf-8").lower()) class ControlledDownloadTests(unittest.TestCase): def test_reader_rehashes_controlled_file(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) path = root / "outputs" / "report.xlsx" path.parent.mkdir() path.write_bytes(b"trusted-xlsx") payload = path.read_bytes() descriptor = ArtifactDescriptor( "monthly_xlsx", "月报.xlsx", "outputs/report.xlsx", hashlib.sha256(payload).hexdigest(), len(payload), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) self.assertEqual(ControlledProjectArtifactReader(root).read(descriptor), payload) def test_reader_rejects_hash_mismatch_and_parent_escape(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) path = root / "report.xlsx" path.write_bytes(b"changed") bad_hash = ArtifactDescriptor( "daily_xlsx", "daily.xlsx", "report.xlsx", "0" * 64, len(b"changed"), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) with self.assertRaisesRegex(Exception, "完整性"): ControlledProjectArtifactReader(root).read(bad_hash) escaped = ArtifactDescriptor( "daily_xlsx", "daily.xlsx", "../report.xlsx", "0" * 64, 1, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) with self.assertRaisesRegex(Exception, "文件身份"): ControlledProjectArtifactReader(root).read(escaped) def test_daily_download_reads_and_reverifies_committed_object(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) source = root / "7.27.xlsx" source.write_bytes(b"trusted-daily-xlsx") store = ManagedObjectStore( FilesystemObjectBackend(root / "objects", create=True) ) stored = store.upload_committed( job_id="arrjob-download-001", attempt_no=1, role="daily_report", source=source, original_filename=source.name, ) descriptor = ArtifactDescriptor( "daily_xlsx", source.name, stored.object_key, stored.sha256, stored.byte_size, stored.mime_type, ) reader = RoutedArtifactReader( daily_reader=ManagedObjectArtifactReader(store), local_reader=ControlledProjectArtifactReader(root), ) self.assertEqual(reader.read(descriptor), source.read_bytes()) bad = ArtifactDescriptor( descriptor.file_kind, descriptor.original_filename, descriptor.storage_key, "0" * 64, descriptor.byte_size, descriptor.mime_type, ) with self.assertRaisesRegex(Exception, "完整性"): reader.read(bad) class BookingHeaderI18nContractTests(unittest.TestCase): def test_booking_header_errors_have_three_locale_messages(self) -> None: script = (STATIC_ROOT / "i18n.js").read_text(encoding="utf-8") expected = { "BOOKING_EXCEL_HEADERS_MISSING": "error.booking_headers_missing", "BOOKING_EXCEL_HEADERS_AMBIGUOUS": "error.booking_headers_ambiguous", } for code, key in expected.items(): self.assertIn(f'{code}: "{key}"', script) match = re.search( rf'^\s*"{re.escape(key)}": (\[.*\]),$', script, re.MULTILINE, ) self.assertIsNotNone(match, key) values = json.loads(match.group(1)) self.assertEqual(len(values), 3, key) self.assertTrue(all(values), key) self.assertRegex(values[2], r"[\u0e00-\u0e7f]", key) if __name__ == "__main__": unittest.main()
版本