Files
homepage.veen.world/tests/unit/test_navigation_template.py
Kevin Veen-Birkenbach 3f30621630 feat(assets): probe-first resolver + SPOT for IMAGE_NAME/PORT + README screenshot
Probe-first asset resolution (regression fix)
---------------------------------------------

cache_manager.cache_file() returned either a relative cache path
(success) or None (failure). The previous app.py fallback
asset['cache'] = cached or asset['source'] mixed both types into
one field, which the template wrapped in url_for('static', ...)
regardless — producing broken
/static/https://file.infinito.nexus/.../logo.png URLs whenever the
source couldn't be downloaded.

- New app/utils/asset_resolver.py: HEAD-probes the URL (3 s
  timeout, image/* content type). On hit, embed directly via a
  new external_url field — no download required. On miss, fall
  back to cache_manager.cache_file. If that also fails, expose
  the source URL via external_url so the browser shows the alt
  text instead of an empty src.
- app.py exposes an asset_src(asset) context processor that
  picks external_url first, then url_for('static', cache),
  so the template never wraps an absolute URL in a static prefix.
- Templates (base, navigation, card) switch to asset_src(...) and
  gate the card image branch on cache or external_url.
- 16 unit tests cover every probe/cache/fallback branch; one live
  integration test exercises the canonical
  https://file.infinito.nexus/assets/img/logo.png to prove the
  probe-first path works end-to-end (cache dir stays empty).
- config.sample.yaml: new Infinito.Nexus card driven by the same
  canonical asset URL.

Single source of truth for IMAGE_NAME and PORT
----------------------------------------------

- env.example is now the only place the literal values live.
- Makefile and docker-compose.yml reference \$(IMAGE_NAME) /
  \${IMAGE_NAME:?…} (same for PORT); no defaults, no silent
  fallbacks.
- New make env / make config bootstrap .env / app/config.yaml
  from their checked-in templates. Idempotent.
- All container-using targets depend on the two bootstrap targets
  so a fresh checkout runs in a single invocation.
- Recipes source .env at recipe-execution time so they pick up a
  freshly bootstrapped .env in the same make invocation.

README
------

- Screenshot added under the title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:19:15 +02:00

79 lines
2.5 KiB
Python

import unittest
from html.parser import HTMLParser
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
class AnchorCollector(HTMLParser):
def __init__(self):
super().__init__()
self.anchors = []
def handle_starttag(self, tag, attrs):
if tag == "a":
self.anchors.append(dict(attrs))
class TestNavigationTemplate(unittest.TestCase):
def test_top_level_dropdowns_have_bootstrap_toggle_attribute(self):
template_dir = Path(__file__).resolve().parents[2] / "app" / "templates"
environment = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(),
)
environment.globals["url_for"] = lambda _endpoint, filename: (
f"/static/{filename}"
)
environment.globals["asset_src"] = lambda asset: (
(asset or {}).get("external_url")
or (
f"/static/{(asset or {}).get('cache')}"
if (asset or {}).get("cache")
else ""
)
)
rendered = environment.get_template("moduls/navigation.html.j2").render(
menu_type="header",
platform={
"titel": "Portfolio",
"logo": {"cache": "logo.png"},
},
navigation={
"header": {
"children": [
{
"name": "Apps",
"description": "Application menu",
"icon": {"class": "fa-solid fa-grid"},
"children": [
{
"name": "Example",
"description": "Example app",
"icon": {"class": "fa-solid fa-link"},
"url": "https://example.test",
}
],
}
]
}
},
)
parser = AnchorCollector()
parser.feed(rendered)
dropdown_toggles = [
anchor
for anchor in parser.anchors
if "nav-link" in anchor.get("class", "")
and "dropdown-toggle" in anchor.get("class", "")
]
self.assertEqual(len(dropdown_toggles), 1)
self.assertEqual(dropdown_toggles[0].get("data-bs-toggle"), "dropdown")
if __name__ == "__main__":
unittest.main()