from __future__ import annotations import re import subprocess import sys from pathlib import Path import httpx import pytest from app.main import create_app ROOT = Path(__file__).resolve().parents[3] PORTAL = ROOT / "app" / "static" / "portal" @pytest.mark.asyncio async def test_portal_root_redirects_to_public_home() -> None: transport = httpx.ASGITransport(app=create_app()) async with httpx.AsyncClient( transport=transport, base_url="http://test", follow_redirects=False ) as client: response = await client.get("/") assert response.status_code in {302, 307} assert response.headers["location"] == "/portal/guest/home/" @pytest.mark.asyncio @pytest.mark.parametrize( "path", [ "/portal/guest/home/", "/portal/guest/products/", "/portal/guest/product-detail/?code=510300", "/portal/customer/login/", "/portal/customer/dashboard/", "/portal/customer/holdings/", "/portal/customer/profit-loss/", "/portal/customer/orders/", "/portal/customer/transactions/", "/portal/customer/cash-ledger/", "/portal/customer/risk-questionnaire/", "/portal/employee-console/login/", "/portal/employee-console/workspace/", "/portal/employee-risk/dashboard/", ], ) async def test_public_portal_pages_are_served(path: str) -> None: transport = httpx.ASGITransport(app=create_app()) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get(path) assert response.status_code == 200 assert 'lang="zh-CN"' in response.text assert '' in response.text def test_every_portal_page_has_local_js_and_css_entry() -> None: pages = list(PORTAL.glob("*/*/index.html")) assert pages for page in pages: page_name = page.parent.name assert (page.parent / f"{page_name}.js").is_file(), page assert (page.parent / f"{page_name}.css").is_file(), page def test_business_pages_do_not_call_fetch_directly() -> None: direct_fetch_files = [ path.relative_to(PORTAL).as_posix() for path in PORTAL.rglob("*.js") if "fetch(" in path.read_text(encoding="utf-8") ] assert direct_fetch_files == ["common/api-client.js"] def test_nl2sql_page_separates_query_values_and_generated_sql() -> None: html = (PORTAL / "employee-operations" / "nl2sql" / "index.html").read_text( encoding="utf-8" ) source = (PORTAL / "employee-operations" / "nl2sql" / "nl2sql.js").read_text( encoding="utf-8" ) assert 'data-view="general"' in html assert "最终查询结果" in source assert "AI 生成的 SQL" in source assert "run?.result?.sql" in source assert "data?.rows" in source assert "运行编号" not in source assert "错误码" not in source def test_promotion_page_uses_resizable_uniform_fields_and_cache_busting() -> None: html = (PORTAL / "employee-operations" / "promotion" / "index.html").read_text( encoding="utf-8" ) css = (PORTAL / "employee-operations" / "promotion" / "promotion.css").read_text( encoding="utf-8" ) source = ( PORTAL / "employee-operations" / "promotion" / "promotion.js" ).read_text(encoding="utf-8") assert "promotion-page" in html assert "promotion-attachments-grid" in html assert "promotion.css?v=20260914-layout2" in html assert "promotion.js?v=20260914-layout2" in html assert "height: 72px" in css assert "resize: vertical" in css assert "justify-content: center" in css assert "autosizeTextarea" not in source assert "function validateFormats(formats)" in source assert "最多选择两种输出格式" in source assert "validateFormats(selectedFormats())" in source api_source = (PORTAL / "common" / "api-client.js").read_text(encoding="utf-8") assert ( "PROMOTION_GENERATE: { method: 'POST', " "path: '/api/v1/fund-promotion-materials/{taskNo}/generations', " "idempotent: true, timeout: 120000 }" ) in api_source def test_nl2sql_page_has_aligned_workspace_spacing_and_cache_busting() -> None: html = (PORTAL / "employee-operations" / "nl2sql" / "index.html").read_text( encoding="utf-8" ) css = (PORTAL / "employee-operations" / "nl2sql" / "nl2sql.css").read_text( encoding="utf-8" ) assert "nl2sql-page" in html assert "nl2sql.css?v=20260914-layout3" in html assert "nl2sql.js?v=20260914-layout3" in html assert "padding: 28px 32px 32px" in css assert "gap: 32px" in css assert "resize: vertical" in css assert "padding: 20px" in css assert "border-radius: var(--radius-md)" in css def test_api_client_registers_all_trading_endpoint_ids() -> None: source = (PORTAL / "common" / "api-client.js").read_text(encoding="utf-8") for endpoint_id in ("T001", "T002", "T003", "T004", "T005", "T006", "T007", "T008", "T009"): assert f"{endpoint_id}:" in source def test_product_detail_preserves_customer_session_for_trade_entry() -> None: html = (PORTAL / "guest" / "product-detail" / "index.html").read_text(encoding="utf-8") source = (PORTAL / "guest" / "product-detail" / "product-detail.js").read_text( encoding="utf-8" ) dashboard = (PORTAL / "customer" / "dashboard" / "dashboard.js").read_text( encoding="utf-8" ) assert 'data-trade-action' in html assert "getAuthContext" in source assert "textContent = '进入交易'" in source assert "action=trade" in source assert "productInput.value = productCode" in dashboard def test_api_client_registers_onboarding_risk_and_admin_endpoints() -> None: source = (PORTAL / "common" / "api-client.js").read_text(encoding="utf-8") for endpoint_id in ( "ONB001", "ONB002", "RK001", "RK002", "RK003", "RK004", "RK005", "RK006", "RK007", "RK008", "RK009", "RK010", "RK011", "RK012", "RK013", "RK014", "RK015", "A002", "A003", "A004", "A005", "A006", "A012", "A033", "A035", "A036", "A037", "A038", "A039", "A040", ): assert f"{endpoint_id}:" in source def test_no_portal_page_includes_the_same_script_twice() -> None: """同一个入口 JS 被引两次(哪怕 `?v=` 不同)会让页面出现两份顶部导航。 浏览器按**完整 URL** 去重:`x.js?v=A` 与 `x.js?v=B` 是两个模块、**各执行一次**。 入口里的 `mountShell()` 于是跑两遍,插入两份 header / footer —— 2026-09-14 `employee-console/workspace/index.html` 就这么写过:合并时 两个分支各自把同一行的版本号换成新的,两边都被保留,成了一条重复的 `