feat: govern accessible appearance overrides

This commit is contained in:
2026-08-20 10:50:55 +02:00
parent 0fae09ba3c
commit f11c675d11
14 changed files with 796 additions and 19 deletions
+88
View File
@@ -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"],