import pytest from pydantic import ValidationError from backend.errors import DestinationDeniedError, InvalidInputError from backend.settings import Settings, canonicalize_url, validate_confluence_url def test_canonicalize_url_valid(): assert canonicalize_url("https://approved.example.com") == "https://approved.example.com" assert canonicalize_url("https://approved.example.com/") == "https://approved.example.com" assert canonicalize_url("https://approved.example.com:443") == "https://approved.example.com" assert canonicalize_url("http://approved.example.com:80") == "http://approved.example.com" assert canonicalize_url("https://approved.example.com:8443/wiki/") == "https://approved.example.com:8443/wiki" assert canonicalize_url("HTTPS://EXAMPLE.COM/Wiki") == "https://example.com/Wiki" def test_canonicalize_url_rejects_invalids(): with pytest.raises(InvalidInputError, match="userinfo"): canonicalize_url("https://user:pass@example.com") with pytest.raises(InvalidInputError, match="fragment"): canonicalize_url("https://example.com/#frag") with pytest.raises(InvalidInputError, match="query"): canonicalize_url("https://example.com/?query=1") with pytest.raises(InvalidInputError, match="traversal"): canonicalize_url("https://example.com/foo/../bar") with pytest.raises(InvalidInputError, match="traversal"): canonicalize_url("https://example.com/foo/%2e%2e/bar") with pytest.raises(InvalidInputError, match="traversal"): canonicalize_url("https://example.com/%2fetc/passwd") with pytest.raises(InvalidInputError, match="scheme"): canonicalize_url("ftp://example.com") with pytest.raises(InvalidInputError, match="empty"): canonicalize_url(" ") # 8 KiB limit with pytest.raises(InvalidInputError, match="limit"): canonicalize_url("https://example.com/" + "a" * 8200) def test_validate_confluence_url(): approved = [ "https://approved.example.com", "https://corp.example.com/wiki", ] # Exact match assert validate_confluence_url("https://approved.example.com", approved) == "https://approved.example.com" assert validate_confluence_url("https://approved.example.com/", approved) == "https://approved.example.com" assert validate_confluence_url("https://corp.example.com/wiki", approved) == "https://corp.example.com/wiki" # Context subpath assert validate_confluence_url("https://corp.example.com/wiki/sub", approved) == "https://corp.example.com/wiki/sub" # Reject prefix lookalike: /wikileaks must NOT match /wiki with pytest.raises(DestinationDeniedError, match="denied"): validate_confluence_url("https://corp.example.com/wikileaks", approved) # Reject different host with pytest.raises(DestinationDeniedError, match="denied"): validate_confluence_url("https://malicious.example.com", approved) # Reject different scheme with pytest.raises(DestinationDeniedError, match="denied"): validate_confluence_url("http://approved.example.com", approved) # -- Admission queue settings validators (docs/QUEUE_SPECIFICATION.md section 4) -- def test_queue_reservation_seconds_defaults_and_bounds(): assert Settings().queue_reservation_seconds == 45.0 Settings(queue_reservation_seconds=30.0) Settings(queue_reservation_seconds=60.0) with pytest.raises(ValidationError): Settings(queue_reservation_seconds=29.9) with pytest.raises(ValidationError): Settings(queue_reservation_seconds=60.1) def test_queue_heartbeat_seconds_defaults_and_bounds(): assert Settings().queue_heartbeat_seconds == 15.0 Settings(queue_heartbeat_seconds=5.0) Settings(queue_heartbeat_seconds=60.0) with pytest.raises(ValidationError): Settings(queue_heartbeat_seconds=4.9) with pytest.raises(ValidationError): Settings(queue_heartbeat_seconds=60.1) def test_queue_max_length_defaults_and_bounds(): assert Settings().queue_max_length == 20 Settings(queue_max_length=1) Settings(queue_max_length=100) with pytest.raises(ValidationError): Settings(queue_max_length=0) with pytest.raises(ValidationError): Settings(queue_max_length=101) def test_queue_settings_from_env(monkeypatch): monkeypatch.setenv("CONFLUENCE_WEB_QUEUE_RESERVATION_SECONDS", "50") monkeypatch.setenv("CONFLUENCE_WEB_QUEUE_HEARTBEAT_SECONDS", "20") monkeypatch.setenv("CONFLUENCE_WEB_QUEUE_MAX_LENGTH", "5") settings = Settings.from_env() assert settings.queue_reservation_seconds == 50.0 assert settings.queue_heartbeat_seconds == 20.0 assert settings.queue_max_length == 5 def test_max_deadline_seconds_defaults_and_bounds(): assert Settings().max_deadline_seconds == 900.0 Settings(max_deadline_seconds=60.0, query_timeout_seconds=60.0) Settings(max_deadline_seconds=3600.0) with pytest.raises(ValidationError): Settings(max_deadline_seconds=59.0, query_timeout_seconds=59.0) with pytest.raises(ValidationError): Settings(max_deadline_seconds=3601.0) def test_query_timeout_must_not_exceed_max_deadline(): Settings(query_timeout_seconds=900.0, max_deadline_seconds=900.0) with pytest.raises(ValidationError, match="must not exceed"): Settings(query_timeout_seconds=901.0, max_deadline_seconds=900.0) def test_max_deadline_seconds_from_env(monkeypatch): monkeypatch.setenv("CONFLUENCE_WEB_MAX_DEADLINE_SECONDS", "1200") monkeypatch.setenv("CONFLUENCE_WEB_QUERY_TIMEOUT_SECONDS", "1000") assert Settings.from_env().max_deadline_seconds == 1200.0