diff --git a/pyproject.toml b/pyproject.toml index 8850527..30a9083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,8 @@ [tool.pytest.ini_options] addopts = "--cov=tests --cov-report=term-missing" +markers = [ + "docker: tests that build and exercise the nginx image through Docker", +] [tool.coverage.run] omit = ["tests/test_video_service.py"] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e7f6143 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +"""Shared test-environment handling for optional Docker integration tests.""" + +import shutil +import subprocess + +import pytest + + +def pytest_runtest_setup(item): + if "docker" not in item.keywords: + return + docker = shutil.which("docker") + if docker is None: + pytest.skip("Docker integration tests require the docker executable") + result = subprocess.run( + [docker, "info"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False + ) + if result.returncode != 0: + pytest.skip("Docker integration tests require access to the Docker daemon") diff --git a/tests/test_generated_infra.py b/tests/test_generated_infra.py index 2addd7b..6a2e797 100644 --- a/tests/test_generated_infra.py +++ b/tests/test_generated_infra.py @@ -5,6 +5,8 @@ import pytest import requests +pytestmark = pytest.mark.docker + # Configuration MANIFEST_PATH = 'content/videos.json' CONTENT_DIR = 'content' diff --git a/tests/test_static_contracts.py b/tests/test_static_contracts.py new file mode 100644 index 0000000..fc398d7 --- /dev/null +++ b/tests/test_static_contracts.py @@ -0,0 +1,96 @@ +"""Deterministic contract tests for the static video service. + +These tests intentionally exercise the files that are packaged into the nginx +image without requiring Docker, a network service, or video decoding. +""" + +import json +import re +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).parents[1] +CONTENT = ROOT / "content" +MANIFEST = CONTENT / "videos.json" +NGINX = ROOT / "nginx.conf" +DOCKERFILE = ROOT / "Dockerfile" +INDEX = CONTENT / "index.html" +GUIDE = CONTENT / "guide.html" + + +def load_manifest(): + with MANIFEST.open(encoding="utf-8") as handle: + return json.load(handle) + + +def test_docker_image_packages_the_documented_content_root(): + dockerfile = DOCKERFILE.read_text(encoding="utf-8") + assert "COPY content/ /usr/share/nginx/html/" in dockerfile + assert "COPY nginx.conf /etc/nginx/conf.d/default.conf" in dockerfile + assert "EXPOSE 8086" in dockerfile + + +def test_manifest_entries_are_complete_unique_and_backed_by_files(): + entries = load_manifest() + assert isinstance(entries, list) and entries + + filenames = [entry["filename"] for entry in entries] + orders = [entry["order"] for entry in entries] + assert len(filenames) == len(set(filenames)) + assert len(orders) == len(set(orders)) + assert orders == sorted(orders) + + for entry in entries: + assert set(entry) == {"filename", "title", "desc", "tag", "order"} + assert re.fullmatch(r"[^/]+\.(?:mp4|webm|mov)", entry["filename"], re.I) + assert (CONTENT / entry["filename"]).is_file() + assert all(isinstance(entry[field], str) and entry[field].strip() for field in ("title", "desc", "tag")) + assert isinstance(entry["order"], int) and not isinstance(entry["order"], bool) + + +def test_nginx_routes_match_the_packaged_layout_and_documented_endpoints(): + config = NGINX.read_text(encoding="utf-8") + assert 'root /usr/share/nginx/html;' in config + assert "location /videos.json" in config + assert 'alias /videos/videos.json;' in config + assert "location /videos/" in config + assert "alias /videos/;" in config + assert "location /health" in config + + +@pytest.mark.xfail(strict=True, reason="Known production routing mismatch: index.js uses /videos/* while Dockerfile packages content at the nginx document root") +def test_index_uses_the_same_video_urls_as_the_nginx_configuration(): + html = INDEX.read_text(encoding="utf-8") + # This currently fails and records the production routing defect: the + # image contains /usr/share/nginx/html/*, not /videos/*. + assert "fetch(`${API}/videos.json`)" in html + assert "fetch(`${API}/videos/`)" in html + assert "${API}/${v.filename}" in html + + +def test_index_contains_manifest_filter_search_and_modal_contracts(): + html = INDEX.read_text(encoding="utf-8") + for marker in ("videos.json", "filterVideos", "setFilter", "openModal", "closeModal", "searchInput", "videoCount"): + assert marker in html + assert "manifest.sort" in html + assert "(a.order || 999) - (b.order || 999)" in html + + +def test_guide_has_all_documented_sections_and_navigation_handler(): + html = GUIDE.read_text(encoding="utf-8") + for section in ("overview", "local", "cloud", "hpc", "llm", "workbench", "workflow", "faq"): + assert f'id="p-{section}"' in html + assert "function show(" in html + assert "show('overview')" in html + + +@pytest.mark.xfail(strict=True, reason="Several manifest-listed video assets are zero-byte placeholders in the repository") +def test_video_files_are_non_empty_and_have_expected_media_extensions(): + manifest_names = {entry["filename"] for entry in load_manifest()} + content_videos = { + path.name for path in CONTENT.iterdir() if path.suffix.lower() in {".mp4", ".webm", ".mov"} + } + assert manifest_names <= content_videos + assert all((CONTENT / name).stat().st_size > 0 for name in manifest_names) diff --git a/tests/test_video_service.py b/tests/test_video_service.py index 940d6ae..202a610 100644 --- a/tests/test_video_service.py +++ b/tests/test_video_service.py @@ -3,6 +3,8 @@ import pytest import requests +pytestmark = pytest.mark.docker + IMAGE_NAME = "omnibioai-videos-test" CONTAINER_NAME = "omnibioai-videos-test-container" PORT = 8086