"""Tests for the `GET /api/version` endpoint. Covers the configurable `APP_SOURCE_URL` / `APP_LICENSE_URL` env vars introduced during the AGPL-3.0 relicense: the default values, the empty/whitespace fallback, trailing-slash stripping, and — most importantly — the URL guard (http(s) scheme + non-empty host) that prevents a misconfigured/hostile env var from smuggling `javascript:` / `data:` / malformed URLs onto the About-page ``. """ import importlib import sys import pytest from fastapi.testclient import TestClient DEFAULT_SOURCE_URL = "https://github.com/got-feedback/feedBack" DEFAULT_LICENSE_URL = DEFAULT_SOURCE_URL + "/blob/main/LICENSE" @pytest.fixture() def client(tmp_path, monkeypatch): """Minimal server TestClient, with background I/O suppressed. Mirrors the fixture pattern used by `test_correlation_id.py` — fresh server import per test so module-level env captures (if any are ever added) don't leak between tests. """ monkeypatch.setenv("CONFIG_DIR", str(tmp_path)) monkeypatch.setenv("FEEDBACK_SYNC_STARTUP", "1") # Clear ambient overrides so the default-URL assertions are deterministic # regardless of the caller's shell / CI environment. Individual tests # re-set these via their own monkeypatch calls as needed. monkeypatch.delenv("APP_SOURCE_URL", raising=False) monkeypatch.delenv("APP_LICENSE_URL", raising=False) sys.modules.pop("server", None) server = importlib.import_module("server") monkeypatch.setattr(server, "load_plugins", lambda *a, **kw: None) monkeypatch.setattr(server, "startup_scan", lambda: None) with TestClient(server.app) as tc: try: yield tc finally: conn = getattr(getattr(server, "meta_db", None), "conn", None) if conn is not None: getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() def _get(client): r = client.get("/api/version") assert r.status_code == 200, r.text return r.json() def test_default_urls_when_envs_unset(client, monkeypatch): """No env vars set → both URLs are the documented defaults.""" monkeypatch.delenv("APP_SOURCE_URL", raising=False) monkeypatch.delenv("APP_LICENSE_URL", raising=False) body = _get(client) assert body["source_url"] == DEFAULT_SOURCE_URL assert body["license_url"] == DEFAULT_LICENSE_URL assert "version" in body def test_empty_source_url_falls_back_to_default(client, monkeypatch): monkeypatch.setenv("APP_SOURCE_URL", "") body = _get(client) assert body["source_url"] == DEFAULT_SOURCE_URL assert body["license_url"] == DEFAULT_LICENSE_URL def test_whitespace_source_url_falls_back_to_default(client, monkeypatch): monkeypatch.setenv("APP_SOURCE_URL", " \t ") body = _get(client) assert body["source_url"] == DEFAULT_SOURCE_URL def test_configured_source_url_used(client, monkeypatch): monkeypatch.setenv("APP_SOURCE_URL", "https://gitlab.example.com/me/fork") body = _get(client) assert body["source_url"] == "https://gitlab.example.com/me/fork" # license_url defaults to the GitHub-style suffix off the configured source assert body["license_url"] == "https://gitlab.example.com/me/fork/blob/main/LICENSE" def test_source_url_trailing_slash_stripped(client, monkeypatch): monkeypatch.setenv("APP_SOURCE_URL", "https://example.com/repo/") body = _get(client) assert body["source_url"] == "https://example.com/repo" assert body["license_url"] == "https://example.com/repo/blob/main/LICENSE" @pytest.mark.parametrize( "hostile", [ "javascript:alert(1)", "JAVASCRIPT:alert(1)", # case-insensitive scheme guard "data:text/html,", "vbscript:msgbox(1)", "file:///etc/passwd", "ftp://example.com/repo", "//evil.example.com/repo", # protocol-relative "evil.example.com/repo", # no scheme "https://", # http(s) prefix but no host "http://", "https:///path/only", # empty host, has path "http://:80/path", # port-only "netloc", empty hostname "https://:8080", ], ) def test_non_http_source_url_rejected(client, monkeypatch, hostile): """Non-http(s) APP_SOURCE_URL values fall back to the safe default — the UI assigns the result straight to ``, so this is XSS-relevant. """ monkeypatch.setenv("APP_SOURCE_URL", hostile) body = _get(client) assert body["source_url"] == DEFAULT_SOURCE_URL # license_url must follow the (safe) source, never the hostile input assert body["license_url"] == DEFAULT_LICENSE_URL def test_configured_license_url_used(client, monkeypatch): """APP_LICENSE_URL overrides the constructed GitHub-style default — for non-GitHub hosts or non-`main` default branches. """ monkeypatch.setenv("APP_LICENSE_URL", "https://gitlab.example.com/me/fork/-/blob/trunk/LICENSE") body = _get(client) assert body["license_url"] == "https://gitlab.example.com/me/fork/-/blob/trunk/LICENSE" def test_license_url_overrides_independently_of_source_url(client, monkeypatch): """When both envs are set, APP_LICENSE_URL wins (doesn't derive from source).""" monkeypatch.setenv("APP_SOURCE_URL", "https://example.com/src") monkeypatch.setenv("APP_LICENSE_URL", "https://example.org/license.txt") body = _get(client) assert body["source_url"] == "https://example.com/src" assert body["license_url"] == "https://example.org/license.txt" def test_license_url_trailing_slash_stripped(client, monkeypatch): monkeypatch.setenv("APP_LICENSE_URL", "https://example.com/license/") body = _get(client) assert body["license_url"] == "https://example.com/license" @pytest.mark.parametrize( "hostile", [ "javascript:alert(1)", "data:text/html,foo", "vbscript:msgbox(1)", "file:///etc/passwd", "//evil.example.com/license", "license.html", # no scheme "https://", # http(s) prefix but no host "http://", "https:///path", # empty host, has path "http://:80/path", # port-only "netloc", empty hostname "https://:8080", ], ) def test_non_http_license_url_rejected(client, monkeypatch, hostile): monkeypatch.setenv("APP_LICENSE_URL", hostile) body = _get(client) # Falls back to source_url + /blob/main/LICENSE (the constructed default) assert body["license_url"] == DEFAULT_LICENSE_URL def test_empty_license_url_falls_back_to_constructed(client, monkeypatch): monkeypatch.setenv("APP_SOURCE_URL", "https://example.com/repo") monkeypatch.setenv("APP_LICENSE_URL", "") body = _get(client) assert body["license_url"] == "https://example.com/repo/blob/main/LICENSE"