feat: govern accessible appearance overrides
This commit is contained in:
@@ -6556,6 +6556,93 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(final_profile.json()["user"]["appearance"]["palette"], "civic_blue")
|
||||
self.assertEqual(final_profile.json()["user"]["appearance"]["source"], "system_lock")
|
||||
|
||||
def test_governed_custom_appearance_overrides_are_atomic_and_removable(self) -> None:
|
||||
headers, _ = self._login()
|
||||
document = {
|
||||
"schema_version": "1",
|
||||
"light": {
|
||||
"accent": "#245f91", "accent_foreground": "#ffffff",
|
||||
"surface": "#ffffff", "surface_foreground": "#303135",
|
||||
"success": "#d8eee8", "success_foreground": "#315f55",
|
||||
"info": "#dce9f3", "info_foreground": "#294a61",
|
||||
"warning": "#ffe1a3", "warning_foreground": "#593700",
|
||||
"danger": "#f8d1cc", "danger_foreground": "#873c35",
|
||||
},
|
||||
"dark": {
|
||||
"accent": "#7ea6c5", "accent_foreground": "#242424",
|
||||
"surface": "#262724", "surface_foreground": "#f1f1f1",
|
||||
"success": "#24473f", "success_foreground": "#d8eee8",
|
||||
"info": "#243d4e", "info_foreground": "#dce9f3",
|
||||
"warning": "#5a431f", "warning_foreground": "#ffe1a3",
|
||||
"danger": "#4f2d2a", "danger_foreground": "#f8d1cc",
|
||||
},
|
||||
}
|
||||
initially_denied = self.client.patch(
|
||||
"/api/v1/auth/profile",
|
||||
headers=headers,
|
||||
json={"ui_preferences": {"appearance_overrides": document}},
|
||||
)
|
||||
self.assertEqual(initially_denied.status_code, 422, initially_denied.text)
|
||||
|
||||
system = self.client.get("/api/v1/admin/system/settings", headers=headers).json()
|
||||
enabled = self.client.patch(
|
||||
"/api/v1/admin/system/settings",
|
||||
headers=headers,
|
||||
json={
|
||||
"default_locale": system["default_locale"],
|
||||
"allow_tenant_custom_groups": system["allow_tenant_custom_groups"],
|
||||
"allow_tenant_custom_roles": system["allow_tenant_custom_roles"],
|
||||
"allow_tenant_api_keys": system["allow_tenant_api_keys"],
|
||||
"appearance_custom_overrides_allowed": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(enabled.status_code, 200, enabled.text)
|
||||
self.assertTrue(enabled.json()["appearance_custom_overrides_allowed"])
|
||||
|
||||
tenant = self.client.get("/api/v1/admin/tenant/settings", headers=headers).json()
|
||||
self.assertIsNone(tenant["appearance_custom_overrides_allowed"])
|
||||
self.assertTrue(tenant["effective_appearance_custom_overrides_allowed"])
|
||||
saved = self.client.patch(
|
||||
"/api/v1/auth/profile",
|
||||
headers=headers,
|
||||
json={"ui_preferences": {"appearance_overrides": document}},
|
||||
)
|
||||
self.assertEqual(saved.status_code, 200, saved.text)
|
||||
self.assertEqual(saved.json()["user"]["appearance"]["custom_overrides"], document)
|
||||
|
||||
invalid = {
|
||||
**document,
|
||||
"light": {**document["light"], "accent_foreground": document["light"]["accent"]},
|
||||
}
|
||||
rejected = self.client.patch(
|
||||
"/api/v1/auth/profile",
|
||||
headers=headers,
|
||||
json={"ui_preferences": {"appearance_overrides": invalid}},
|
||||
)
|
||||
self.assertEqual(rejected.status_code, 422, rejected.text)
|
||||
unchanged = self.client.get("/api/v1/auth/profile", headers=headers).json()
|
||||
self.assertEqual(unchanged["user"]["appearance"]["custom_overrides"], document)
|
||||
|
||||
blocked = self.client.patch(
|
||||
"/api/v1/admin/tenant/settings",
|
||||
headers=headers,
|
||||
json={
|
||||
"default_locale": tenant["default_locale"],
|
||||
"appearance_custom_overrides_allowed": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(blocked.status_code, 200, blocked.text)
|
||||
self.assertFalse(blocked.json()["effective_appearance_custom_overrides_allowed"])
|
||||
inactive = self.client.get("/api/v1/auth/profile", headers=headers).json()
|
||||
self.assertIsNone(inactive["user"]["appearance"]["custom_overrides"])
|
||||
removed = self.client.patch(
|
||||
"/api/v1/auth/profile",
|
||||
headers=headers,
|
||||
json={"ui_preferences": {"appearance_overrides": None}},
|
||||
)
|
||||
self.assertEqual(removed.status_code, 200, removed.text)
|
||||
self.assertIsNone(removed.json()["user"]["ui_preferences"]["appearance_overrides"])
|
||||
|
||||
def test_profile_refresh_and_system_role_protection_model(self) -> None:
|
||||
headers, _ = self._login()
|
||||
profile = self.client.patch(
|
||||
@@ -6591,6 +6678,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
"sticky_section_sidebars": False,
|
||||
"theme": "dark",
|
||||
"palette": "civic_blue",
|
||||
"appearance_overrides": None,
|
||||
"navigation": {
|
||||
"contract_version": "1",
|
||||
"order": ["files.navigation.files", "mail.navigation.mail"],
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from govoplan_core.core.appearance import resolve_effective_appearance, update_appearance_settings
|
||||
import pytest
|
||||
|
||||
from govoplan_core.core.appearance import (
|
||||
normalize_appearance_overrides,
|
||||
resolve_effective_appearance,
|
||||
update_appearance_custom_overrides_policy,
|
||||
update_appearance_settings,
|
||||
)
|
||||
|
||||
|
||||
def _overrides() -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "1",
|
||||
"light": {
|
||||
"accent": "#245f91", "accent_foreground": "#ffffff",
|
||||
"surface": "#ffffff", "surface_foreground": "#303135",
|
||||
"success": "#d8eee8", "success_foreground": "#315f55",
|
||||
"info": "#dce9f3", "info_foreground": "#294a61",
|
||||
"warning": "#ffe1a3", "warning_foreground": "#593700",
|
||||
"danger": "#f8d1cc", "danger_foreground": "#873c35",
|
||||
},
|
||||
"dark": {
|
||||
"accent": "#7ea6c5", "accent_foreground": "#242424",
|
||||
"surface": "#262724", "surface_foreground": "#f1f1f1",
|
||||
"success": "#24473f", "success_foreground": "#d8eee8",
|
||||
"info": "#243d4e", "info_foreground": "#dce9f3",
|
||||
"warning": "#5a431f", "warning_foreground": "#ffe1a3",
|
||||
"danger": "#4f2d2a", "danger_foreground": "#f8d1cc",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_appearance_precedence_and_inheritance() -> None:
|
||||
@@ -32,6 +61,8 @@ def test_system_lock_wins_and_invalid_values_fail_safe() -> None:
|
||||
"system_default_palette": "civic_blue",
|
||||
"tenant_default_palette": "forest",
|
||||
"inherited_palette": "civic_blue",
|
||||
"custom_overrides": None,
|
||||
"custom_overrides_allowed": False,
|
||||
}
|
||||
fallback = resolve_effective_appearance(
|
||||
system_settings={"appearance": {"default_palette": "unsafe"}},
|
||||
@@ -52,3 +83,45 @@ def test_appearance_settings_reset_without_touching_neighbors() -> None:
|
||||
assert update_appearance_settings(configured, default_palette=None, palette_locked=False) == {
|
||||
"neighbor": {"kept": True}
|
||||
}
|
||||
|
||||
|
||||
def test_custom_overrides_require_system_and_tenant_policy_and_validate_both_modes() -> None:
|
||||
document = _overrides()
|
||||
normalized = normalize_appearance_overrides(document)
|
||||
assert normalized == document
|
||||
decision = resolve_effective_appearance(
|
||||
system_settings={"appearance": {"allow_custom_overrides": True}},
|
||||
tenant_settings={"appearance": {"allow_custom_overrides": True}},
|
||||
user_settings={"ui": {"appearance_overrides": document}},
|
||||
)
|
||||
assert decision.custom_overrides_allowed is True
|
||||
assert decision.custom_overrides == document
|
||||
|
||||
blocked = resolve_effective_appearance(
|
||||
system_settings={"appearance": {"allow_custom_overrides": True}},
|
||||
tenant_settings={"appearance": {"allow_custom_overrides": False}},
|
||||
user_settings={"ui": {"appearance_overrides": document}},
|
||||
)
|
||||
assert blocked.custom_overrides_allowed is False
|
||||
assert blocked.custom_overrides is None
|
||||
|
||||
invalid = _overrides()
|
||||
invalid["dark"]["danger"] = invalid["dark"]["warning"] # type: ignore[index]
|
||||
with pytest.raises(ValueError, match="visibly distinct"):
|
||||
normalize_appearance_overrides(invalid)
|
||||
|
||||
low_contrast = _overrides()
|
||||
low_contrast["light"]["accent_foreground"] = "#245f91" # type: ignore[index]
|
||||
with pytest.raises(ValueError, match="WCAG AA"):
|
||||
normalize_appearance_overrides(low_contrast)
|
||||
|
||||
|
||||
def test_custom_override_policy_update_preserves_neighboring_appearance_settings() -> None:
|
||||
configured = update_appearance_custom_overrides_policy(
|
||||
{"appearance": {"default_palette": "forest"}},
|
||||
allowed=True,
|
||||
)
|
||||
assert configured == {"appearance": {"default_palette": "forest", "allow_custom_overrides": True}}
|
||||
assert update_appearance_custom_overrides_policy(configured, allowed=None) == {
|
||||
"appearance": {"default_palette": "forest"}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user