from io import BytesIO from types import SimpleNamespace import pytest from fastapi import HTTPException from starlette.datastructures import UploadFile from app.gateway.routers import app_identity as app_identity_router from deerflow.config.system_settings import AppIdentitySettings, SystemSettings PNG = bytes.fromhex( "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4" "890000000a49444154789c63000100000500010d0a2db40000000049454e44ae426082" ) SVG = b'' def _store(monkeypatch: pytest.MonkeyPatch, tmp_path): current = {"settings": SystemSettings()} def load(): return current["settings"] def save(settings: SystemSettings) -> SystemSettings: current["settings"] = settings return settings async def admin_user(_request): return SimpleNamespace(system_role="admin") monkeypatch.setattr(app_identity_router, "runtime_home", lambda: tmp_path) monkeypatch.setattr("deerflow.config.system_settings.runtime_home", lambda: tmp_path) monkeypatch.setattr(app_identity_router, "load_system_settings", load) monkeypatch.setattr(app_identity_router, "save_system_settings", save) monkeypatch.setattr("deerflow.config.system_settings.load_system_settings", load) monkeypatch.setattr( "app.gateway.routers.system_settings.get_optional_user_from_request", admin_user, ) return current def test_new_settings_leave_app_identity_empty() -> None: assert SystemSettings().app_identity == AppIdentitySettings() assert SystemSettings().app_identity.header_title_font_mode == "adaptive" assert SystemSettings().app_identity.header_title_font_size == 30 assert SystemSettings().app_identity.light_red_header_background_filename is None assert SystemSettings().app_identity.light_red_nav_active_filename is None @pytest.mark.asyncio async def test_non_admin_cannot_change_app_identity(monkeypatch: pytest.MonkeyPatch) -> None: async def ordinary_user(_request): return SimpleNamespace(system_role="user") monkeypatch.setattr( "app.gateway.routers.system_settings.get_optional_user_from_request", ordinary_user, ) with pytest.raises(HTTPException) as error: await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="新系统", remove_logo=False, logo=None, ) assert error.value.status_code == 403 @pytest.mark.asyncio async def test_admin_saves_name_without_replacing_the_static_logo( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: current = _store(monkeypatch, tmp_path) response = await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name=" 研判中心 ", remove_logo=False, logo=None, ) assert response.system_name == "研判中心" assert response.has_logo is False assert response.logo_url is None assert current["settings"].app_identity.logo_filename is None @pytest.mark.asyncio async def test_admin_saves_header_title_font_settings( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: current = _store(monkeypatch, tmp_path) response = await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=None, header_title_font_mode="fixed", header_title_font_size=34, ) assert response.header_title_font_mode == "fixed" assert response.header_title_font_size == 34 assert current["settings"].app_identity.header_title_font_mode == "fixed" assert current["settings"].app_identity.header_title_font_size == 34 renamed = await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="新研判中心", remove_logo=False, logo=None, ) assert renamed.header_title_font_mode == "fixed" assert renamed.header_title_font_size == 34 @pytest.mark.asyncio async def test_header_title_fixed_font_size_is_bounded( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: _store(monkeypatch, tmp_path) with pytest.raises(HTTPException) as error: await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=None, header_title_font_mode="fixed", header_title_font_size=41, ) assert error.value.status_code == 422 @pytest.mark.asyncio async def test_uploaded_logo_is_served_separately_from_generated_pages( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: current = _store(monkeypatch, tmp_path) upload = UploadFile(file=BytesIO(PNG), filename="mark.png") saved = await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=upload, ) assert saved.has_logo is True assert saved.logo_url is not None assert saved.logo_url.startswith("/api/public/app-identity/logo?v=") logo_file = tmp_path / "app-identity" / current["settings"].app_identity.logo_filename assert logo_file.is_file() assert not (tmp_path / "generated-page-branding").exists() served = await app_identity_router.get_app_identity_logo() assert served.media_type == "image/png" @pytest.mark.asyncio async def test_invalid_logo_is_rejected(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: _store(monkeypatch, tmp_path) upload = UploadFile(file=BytesIO(b"not-an-image"), filename="mark.gif") with pytest.raises(HTTPException) as error: await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=upload, ) assert error.value.status_code == 422 @pytest.mark.asyncio async def test_red_navigation_images_can_be_uploaded_and_served( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: current = _store(monkeypatch, tmp_path) saved = await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=None, light_red_header_background=UploadFile( file=BytesIO(SVG), filename="header.svg", ), light_red_nav_active=UploadFile( file=BytesIO(PNG), filename="active.png", ), ) assert saved.has_light_red_header_background is True assert saved.has_light_red_nav_active is True assert saved.light_red_header_background_url is not None assert saved.light_red_header_background_url.startswith( "/api/public/app-identity/light-red-header-background?v=" ) assert saved.light_red_nav_active_url is not None assert saved.light_red_nav_active_url.startswith( "/api/public/app-identity/light-red-nav-active?v=" ) identity = current["settings"].app_identity header_file = tmp_path / "app-identity" / identity.light_red_header_background_filename active_file = tmp_path / "app-identity" / identity.light_red_nav_active_filename assert header_file.is_file() assert header_file.suffix == ".svg" assert active_file.is_file() served_header = await app_identity_router.get_light_red_header_background() served_active = await app_identity_router.get_light_red_nav_active() assert served_header.media_type == "image/svg+xml" assert served_header.headers["content-security-policy"].startswith("sandbox;") assert served_active.media_type == "image/png" @pytest.mark.asyncio async def test_restoring_red_navigation_images_uses_built_in_defaults( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: current = _store(monkeypatch, tmp_path) await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=None, light_red_header_background=UploadFile( file=BytesIO(PNG), filename="header.png", ), light_red_nav_active=UploadFile( file=BytesIO(PNG), filename="active.png", ), ) identity = current["settings"].app_identity header_file = tmp_path / "app-identity" / identity.light_red_header_background_filename active_file = tmp_path / "app-identity" / identity.light_red_nav_active_filename restored = await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=None, remove_light_red_header_background=True, remove_light_red_nav_active=True, ) assert restored.has_light_red_header_background is False assert restored.has_light_red_nav_active is False assert header_file.exists() is False assert active_file.exists() is False @pytest.mark.asyncio async def test_clearing_app_identity_removes_only_its_logo( monkeypatch: pytest.MonkeyPatch, tmp_path, ) -> None: current = _store(monkeypatch, tmp_path) upload = UploadFile(file=BytesIO(PNG), filename="mark.png") await app_identity_router.update_app_identity_settings( SimpleNamespace(), system_name="研判中心", remove_logo=False, logo=upload, ) logo_file = tmp_path / "app-identity" / current["settings"].app_identity.logo_filename cleared = await app_identity_router.delete_app_identity_settings(SimpleNamespace()) assert cleared.system_name == "" assert cleared.header_title_font_mode == "adaptive" assert cleared.header_title_font_size == 30 assert cleared.has_logo is False assert logo_file.exists() is False with pytest.raises(HTTPException) as error: await app_identity_router.get_app_identity_logo() assert error.value.status_code == 404