diff --git a/app/theme_pro.py b/app/theme_pro.py index 2e2e9ce..8a34a4b 100644 --- a/app/theme_pro.py +++ b/app/theme_pro.py @@ -23,13 +23,17 @@ def env_for(theme): Lets a theme override shared macros (_professional_components.html) while inheriting everything else. Cached: one env per theme across large builds. + Only built-in theme names are accepted; callers are expected to enforce the + THEMES whitelist too (render does), this is defense in depth. """ + if theme not in THEMES:raise ValueError('未知主题') env=_ENVS.get(theme) if env is None: theme_dir=ROOT/'themes'/theme - search=[str(theme_dir),str(ROOT/'themes')] if theme_dir.is_dir() else [str(ROOT/'themes')] + canonical=theme_dir.is_dir() + search=[str(theme_dir),str(ROOT/'themes')] if canonical else [str(ROOT/'themes')] env=Environment(loader=FileSystemLoader(search),autoescape=select_autoescape(['html','xml'])) - _ENVS[theme]=env + if canonical:_ENVS[theme]=env # degraded envs are not cached return env TITLES={'product':'产品中心','vehicle':'车型中心','post':'新闻资讯','case':'项目案例','brand':'品牌中心','service':'服务与解决方案'} diff --git a/tests/test_theme_registry.py b/tests/test_theme_registry.py index d6b5dc4..7a73c17 100644 --- a/tests/test_theme_registry.py +++ b/tests/test_theme_registry.py @@ -54,3 +54,40 @@ def test_env_for_is_cached_per_theme(): def test_env_for_pro_template_loads_unprefixed(): from app import theme_pro assert theme_pro.env_for('industrial').get_template('pro.html') is not None + +def test_env_for_rejects_unknown_theme(): + import pytest + from app import theme_pro + with pytest.raises(ValueError): + theme_pro.env_for('../../etc') + with pytest.raises(ValueError): + theme_pro.env_for('no-such-theme') + +def test_env_for_theme_macro_shadows_shared_root(tmp_path, monkeypatch): + # The core mechanism: a theme-local _professional_components.html wins over + # the shared one at themes/ root. + from app import theme_pro + themes = tmp_path / 'themes' + for sub, marker in (('industrial', 'theme-local'), ('_root_macros', None)): + d = themes / sub + d.mkdir(parents=True) + (themes / '_professional_components.html').write_text('SHARED', encoding='utf-8') + (themes / 'industrial' / '_professional_components.html').write_text('THEME-LOCAL', encoding='utf-8') + monkeypatch.setattr(theme_pro, 'ROOT', tmp_path) + monkeypatch.setattr(theme_pro, '_ENVS', {}) + tpl = theme_pro.env_for('industrial').get_template('_professional_components.html') + assert tpl.render().strip() == 'THEME-LOCAL' + +def test_env_for_degraded_branch_not_cached_and_single_path(tmp_path, monkeypatch): + from app import theme_pro + themes = tmp_path / 'themes' + themes.mkdir(parents=True) + monkeypatch.setattr(theme_pro, 'ROOT', tmp_path) + monkeypatch.setattr(theme_pro, '_ENVS', {}) + core.THEMES = ('industrial',) # whitelist a theme whose dir does not exist + try: + env = theme_pro.env_for('industrial') + assert [str(p) for p in env.loader.searchpath] == [str(themes)] + assert 'industrial' not in theme_pro._ENVS + finally: + core.THEMES = core.discover_themes()