mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 18:29:21 +00:00 
			
		
		
		
	feat(frontend): rename inj roles to sys-front-*, add sys-svc-cdn, cache-busting lookup
Introduce sys-svc-cdn (cdn_paths/cdn_urls/cdn_dirs) and ensure CDN directories + latest symlink. Rename sys-srv-web-inj-* → sys-front-inj-*; update includes/templates; serve shared/per-app CSS & JS via CDN. Add lookup_plugins/local_mtime_qs.py for mtime-based cache busting; split CSS into default.css/bootstrap.css + optional per-app style.css. CSP: use style-src-elem; drop unsafe-inline for styles. Services: fix SYS_SERVICE_ALL_ENABLED bool and controlled flush. BREAKING CHANGE: role names changed; replace includes and references accordingly. Conversation: https://chatgpt.com/share/68b55494-9ec4-800f-b559-44707029141d
This commit is contained in:
		| @@ -12,6 +12,7 @@ class TestCspConfigurationConsistency(unittest.TestCase): | ||||
|         'script-src', | ||||
|         'script-src-elem', | ||||
|         'style-src', | ||||
|         'style-src-elem', | ||||
|         'font-src', | ||||
|         'worker-src', | ||||
|         'manifest-src', | ||||
|   | ||||
| @@ -218,7 +218,7 @@ class TestVariableDefinitions(unittest.TestCase): | ||||
|                                 if var in ( | ||||
|                                     'lookup', 'role_name', 'domains', 'item', 'host_type', | ||||
|                                     'inventory_hostname', 'role_path', 'playbook_dir', | ||||
|                                     'ansible_become_password', 'inventory_dir', 'ansible_memtotal_mb', 'omit', 'group_names' | ||||
|                                     'ansible_become_password', 'inventory_dir', 'ansible_memtotal_mb', 'omit', 'group_names', 'ansible_processor_vcpus' | ||||
|                                 ): | ||||
|                                     continue | ||||
|  | ||||
|   | ||||
							
								
								
									
										50
									
								
								tests/unit/lookup_plugins/test_local_mtime_qs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								tests/unit/lookup_plugins/test_local_mtime_qs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import os | ||||
| import sys | ||||
| import tempfile | ||||
| import time | ||||
| import unittest | ||||
|  | ||||
| # ensure repo root on path | ||||
| ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) | ||||
| sys.path.insert(0, ROOT) | ||||
|  | ||||
| from ansible.errors import AnsibleError  # type: ignore | ||||
| from lookup_plugins.local_mtime_qs import LookupModule | ||||
|  | ||||
|  | ||||
| class TestLocalMtimeQs(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         self.tmpdir = tempfile.TemporaryDirectory() | ||||
|         self.path = os.path.join(self.tmpdir.name, "file.css") | ||||
|         with open(self.path, "w", encoding="utf-8") as f: | ||||
|             f.write("body{}") | ||||
|         # set stable mtime | ||||
|         self.mtime = int(time.time()) - 123 | ||||
|         os.utime(self.path, (self.mtime, self.mtime)) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.tmpdir.cleanup() | ||||
|  | ||||
|     def test_single_path_qs_default(self): | ||||
|         res = LookupModule().run([self.path]) | ||||
|         self.assertEqual(res, [f"?version={self.mtime}"]) | ||||
|  | ||||
|     def test_single_path_epoch(self): | ||||
|         res = LookupModule().run([self.path], mode="epoch") | ||||
|         self.assertEqual(res, [str(self.mtime)]) | ||||
|  | ||||
|     def test_multiple_paths(self): | ||||
|         path2 = os.path.join(self.tmpdir.name, "a.js") | ||||
|         with open(path2, "w", encoding="utf-8") as f: | ||||
|             f.write("// js") | ||||
|         os.utime(path2, (self.mtime + 1, self.mtime + 1)) | ||||
|         res = LookupModule().run([self.path, path2], param="v") | ||||
|         self.assertEqual(res, [f"?v={self.mtime}", f"?v={self.mtime + 1}"]) | ||||
|  | ||||
|     def test_missing_raises(self): | ||||
|         with self.assertRaises(AnsibleError): | ||||
|             LookupModule().run([os.path.join(self.tmpdir.name, "nope.css")]) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     unittest.main() | ||||
| @@ -1,4 +1,3 @@ | ||||
| # tests/unit/roles/srv-web-inj-compose/filter_plugins/test_inj_enabled.py | ||||
| import importlib.util | ||||
| from importlib import import_module | ||||
| from pathlib import Path | ||||
| @@ -8,7 +7,7 @@ import unittest | ||||
| THIS_FILE = Path(__file__) | ||||
| 
 | ||||
| def find_repo_root(start: Path) -> Path: | ||||
|     target_rel = Path("roles") / "sys-srv-web-inj-compose" / "filter_plugins" / "inj_enabled.py" | ||||
|     target_rel = Path("roles") / "sys-front-inj-all" / "filter_plugins" / "inj_enabled.py" | ||||
|     cur = start | ||||
|     for _ in range(12): | ||||
|         if (cur / target_rel).is_file(): | ||||
| @@ -17,7 +16,7 @@ def find_repo_root(start: Path) -> Path: | ||||
|     return start.parents[6] | ||||
| 
 | ||||
| REPO_ROOT = find_repo_root(THIS_FILE) | ||||
| PLUGIN_PATH = REPO_ROOT / "roles" / "sys-srv-web-inj-compose" / "filter_plugins" / "inj_enabled.py" | ||||
| PLUGIN_PATH = REPO_ROOT / "roles" / "sys-front-inj-all" / "filter_plugins" / "inj_enabled.py" | ||||
| 
 | ||||
| # Ensure 'module_utils' is importable under its canonical package name | ||||
| if str(REPO_ROOT) not in sys.path: | ||||
| @@ -1,6 +1,6 @@ | ||||
| # tests/unit/roles/sys-srv-web-inj-compose/filter_plugins/test_inj_snippets.py | ||||
| # tests/unit/roles/sys-front-inj-all/filter_plugins/test_inj_snippets.py | ||||
| """ | ||||
| Unit tests for roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py | ||||
| Unit tests for roles/sys-front-inj-all/filter_plugins/inj_snippets.py | ||||
| 
 | ||||
| - Uses tempfile.TemporaryDirectory for an isolated roles/ tree. | ||||
| - Loads inj_snippets.py by absolute path (no sys.path issues). | ||||
| @@ -22,7 +22,7 @@ class TestInjSnippets(unittest.TestCase): | ||||
|         cls.test_dir = os.path.dirname(__file__) | ||||
|         root = cls.test_dir | ||||
|         inj_rel = os.path.join( | ||||
|             "roles", "sys-srv-web-inj-compose", "filter_plugins", "inj_snippets.py" | ||||
|             "roles", "sys-front-inj-all", "filter_plugins", "inj_snippets.py" | ||||
|         ) | ||||
| 
 | ||||
|         while True: | ||||
| @@ -67,7 +67,7 @@ class TestInjSnippets(unittest.TestCase): | ||||
| 
 | ||||
|     @classmethod | ||||
|     def _mkrole(cls, feature, head=False, body=False): | ||||
|         role_dir = os.path.join(cls.roles_dir, f"sys-srv-web-inj-{feature}") | ||||
|         role_dir = os.path.join(cls.roles_dir, f"sys-front-inj-{feature}") | ||||
|         tmpl_dir = os.path.join(role_dir, "templates") | ||||
|         os.makedirs(tmpl_dir, exist_ok=True) | ||||
|         if head: | ||||
							
								
								
									
										0
									
								
								tests/unit/roles/sys-svc-cdn/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/unit/roles/sys-svc-cdn/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| import os | ||||
| import tempfile | ||||
| import unittest | ||||
| import importlib.util | ||||
|  | ||||
| HERE = os.path.abspath(os.path.dirname(__file__)) | ||||
|  | ||||
| def _find_repo_root(start_dir: str, probe_parts: list[str]) -> str: | ||||
|     """ | ||||
|     Walk upwards from start_dir until a path joined with probe_parts exists. | ||||
|     Returns the directory considered the repo root. | ||||
|     """ | ||||
|     cur = os.path.abspath(start_dir) | ||||
|     for _ in range(15):  # plenty of headroom | ||||
|         candidate = os.path.join(cur, *probe_parts) | ||||
|         if os.path.exists(candidate): | ||||
|             return cur | ||||
|         parent = os.path.dirname(cur) | ||||
|         if parent == cur: | ||||
|             break | ||||
|         cur = parent | ||||
|     raise RuntimeError( | ||||
|         f"Could not locate {'/'.join(probe_parts)} starting from {start_dir}" | ||||
|     ) | ||||
|  | ||||
| PROBE = ["roles", "sys-svc-cdn", "filter_plugins", "cdn_paths.py"] | ||||
| ROOT = _find_repo_root(HERE, PROBE) | ||||
|  | ||||
| def _load_module(mod_name: str, rel_path_from_root: str): | ||||
|     """Load a python module from an absolute file path (hyphen-safe).""" | ||||
|     path = os.path.join(ROOT, rel_path_from_root) | ||||
|     if not os.path.isfile(path): | ||||
|         raise FileNotFoundError(path) | ||||
|     spec = importlib.util.spec_from_file_location(mod_name, path) | ||||
|     module = importlib.util.module_from_spec(spec) | ||||
|     assert spec and spec.loader, f"Cannot load spec for {path}" | ||||
|     spec.loader.exec_module(module)  # type: ignore[attr-defined] | ||||
|     return module | ||||
|  | ||||
| cdn_paths_mod = _load_module( | ||||
|     "cdn_paths_mod", os.path.join("roles", "sys-svc-cdn", "filter_plugins", "cdn_paths.py") | ||||
| ) | ||||
| cdn_urls_mod = _load_module( | ||||
|     "cdn_urls_mod", os.path.join("roles", "sys-svc-cdn", "filter_plugins", "cdn_urls.py") | ||||
| ) | ||||
| cdn_dirs_mod = _load_module( | ||||
|     "cdn_dirs_mod", os.path.join("roles", "sys-svc-cdn", "filter_plugins", "cdn_dirs.py") | ||||
| ) | ||||
|  | ||||
| class TestCdnPathsUrlsDirs(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         self.tmp = tempfile.TemporaryDirectory() | ||||
|         self.root = self.tmp.name | ||||
|         self.app = "web-app-desktop" | ||||
|         self.ver = "20250101" | ||||
|  | ||||
|         self.cdn_paths = cdn_paths_mod.cdn_paths | ||||
|         self.cdn_urls = cdn_urls_mod.cdn_urls | ||||
|         self.cdn_dirs = cdn_dirs_mod.cdn_dirs | ||||
|  | ||||
|         self.tree = self.cdn_paths(self.root, self.app, self.ver) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.tmp.cleanup() | ||||
|  | ||||
|     # ---- cdn_paths ---- | ||||
|     def test_paths_shape_and_values(self): | ||||
|         t = self.tree | ||||
|         self.assertTrue(os.path.isabs(t["root"])) | ||||
|         self.assertEqual(t["role"]["id"], self.app) | ||||
|         self.assertEqual(t["role"]["version"], self.ver) | ||||
|         self.assertTrue(t["shared"]["css"].endswith(os.path.join("_shared", "css"))) | ||||
|         self.assertTrue( | ||||
|             t["role"]["release"]["css"].endswith(os.path.join(self.app, self.ver, "css")) | ||||
|         ) | ||||
|  | ||||
|     # ---- cdn_urls ---- | ||||
|     def test_urls_mapping_and_root_trailing_slash(self): | ||||
|         base = "https://cdn.example.com" | ||||
|         urls = self.cdn_urls(self.tree, base) | ||||
|  | ||||
|         # Non-path strings remain untouched | ||||
|         self.assertEqual(urls["role"]["id"], self.app) | ||||
|         self.assertEqual(urls["role"]["version"], self.ver) | ||||
|  | ||||
|         # Paths are mapped to URLs | ||||
|         self.assertTrue(urls["shared"]["js"].startswith(base + "/")) | ||||
|         self.assertTrue(urls["vendor"].startswith(base + "/vendor")) | ||||
|  | ||||
|         # Root always ends with '/' | ||||
|         self.assertEqual(urls["root"], base.rstrip("/") + "/") | ||||
|  | ||||
|     def test_urls_invalid_input_raises(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             self.cdn_urls({}, "https://cdn.example.com") | ||||
|         with self.assertRaises(ValueError): | ||||
|             self.cdn_urls("nope", "https://cdn.example.com")  # type: ignore[arg-type] | ||||
|  | ||||
|     # ---- cdn_dirs ---- | ||||
|     def test_dirs_collects_all_abs_dirs_sorted_unique(self): | ||||
|         dirs = self.cdn_dirs(self.tree) | ||||
|         self.assertIn(os.path.join(self.root, "_shared", "css"), dirs) | ||||
|         self.assertIn(os.path.join(self.root, "roles", self.app, self.ver, "img"), dirs) | ||||
|         self.assertEqual(dirs, sorted(dirs)) | ||||
|         self.assertEqual(len(dirs), len(set(dirs))) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     unittest.main() | ||||
		Reference in New Issue
	
	Block a user