"""Tests for reading open SonarQube findings.""" from __future__ import annotations import base64 import httpx import pytest from ariadne.services import hermes_sonar_client as module CFG = { "sonar_base_url": "http://sonarqube.quality.svc.cluster.local:9000/", "sonar_token": "squ_token", "timeout_seconds": 5, } ISSUE = { "key": "AZ-1", "component": "ariadne:ariadne/services/hermes_code_defects.py", "line": 160, "rule": "python:S1172", "severity": "MAJOR", "type": "CODE_SMELL", "effort": "5min", "message": 'Remove the unused function parameter "cfg".', } class _Response: def __init__(self, status_code=200, payload=None, raises=False): self.status_code = status_code self._payload = payload if payload is not None else {"issues": [ISSUE]} self._raises = raises def json(self): if self._raises: raise ValueError("not json") return self._payload class _Client: """Stands in for httpx.Client, recording the single request made.""" last_url = "" last_params: dict = {} last_headers: dict = {} response = _Response() error: Exception | None = None def __init__(self, *_args, **_kwargs): pass def __enter__(self): return self def __exit__(self, *_args): return False def get(self, url, headers=None, params=None): _Client.last_url = url _Client.last_params = params or {} _Client.last_headers = headers or {} if _Client.error is not None: raise _Client.error return _Client.response @pytest.fixture(autouse=True) def _stub_client(monkeypatch): _Client.response = _Response() _Client.error = None monkeypatch.setattr(module.httpx, "Client", _Client) return _Client def test_a_project_returns_normalized_findings() -> None: issues, error = module.fetch_issues(CFG, "ariadne") assert error is None assert issues == [ { "key": "AZ-1", "path": "ariadne/services/hermes_code_defects.py", "line": 160, "rule": "python:S1172", "severity": "MAJOR", "type": "CODE_SMELL", "effort": "5min", "message": 'Remove the unused function parameter "cfg".', } ] def test_the_token_is_sent_as_basic_auth_with_an_empty_password() -> None: """SonarQube accepts a user token only in that shape.""" module.fetch_issues(CFG, "ariadne") expected = base64.b64encode(b"squ_token:").decode("ascii") assert _Client.last_headers["Authorization"] == f"Basic {expected}" def test_only_unresolved_findings_nobody_has_ruled_on_are_requested() -> None: """A won't-fix carries a human judgement; reopening it wastes tokens.""" module.fetch_issues(CFG, "ariadne") assert _Client.last_params["resolved"] == "false" assert _Client.last_params["statuses"] == "OPEN,CONFIRMED,REOPENED" assert _Client.last_params["componentKeys"] == "ariadne" def test_security_hotspots_are_never_requested() -> None: """Resolving a hotspot is a review decision, not a code change.""" module.fetch_issues(CFG, "ariadne") assert "HOTSPOT" not in _Client.last_params["types"] assert "SECURITY_HOTSPOT" not in _Client.last_params["types"] assert set(_Client.last_params["types"].split(",")) == set(module.ALL_ISSUE_TYPES) def test_the_double_slash_in_the_url_is_avoided() -> None: module.fetch_issues(CFG, "ariadne") assert _Client.last_url == ( "http://sonarqube.quality.svc.cluster.local:9000/api/issues/search" ) def test_configured_types_and_severities_narrow_the_query() -> None: module.fetch_issues( {**CFG, "sonar_types": ["code_smell"], "sonar_severities": ["critical", "major"]}, "ariadne", ) assert _Client.last_params["types"] == "CODE_SMELL" assert _Client.last_params["severities"] == "CRITICAL,MAJOR" def test_unrecognised_types_fall_back_to_every_type() -> None: module.fetch_issues({**CFG, "sonar_types": ["nonsense"]}, "ariadne") assert set(_Client.last_params["types"].split(",")) == set(module.ALL_ISSUE_TYPES) @pytest.mark.parametrize( ("size", "expected"), [(None, "100"), (5, "5"), (9000, "500"), ("junk", "100")], ) def test_the_page_size_is_clamped(size, expected) -> None: module.fetch_issues({**CFG, "sonar_page_size": size}, "ariadne") assert _Client.last_params["ps"] == expected def test_a_finding_with_no_line_is_dropped() -> None: """Nothing can be anchored to a file-level finding, so it is not offered.""" _Client.response = _Response(payload={"issues": [{**ISSUE, "line": None}]}) issues, error = module.fetch_issues(CFG, "ariadne") assert error is None assert issues == [] @pytest.mark.parametrize( "issue", [ {**ISSUE, "key": ""}, {**ISSUE, "component": "ariadne"}, {**ISSUE, "line": True}, "not-a-dict", ], ) def test_an_unusable_finding_is_dropped(issue) -> None: _Client.response = _Response(payload={"issues": [issue]}) assert module.fetch_issues(CFG, "ariadne")[0] == [] def test_a_long_message_is_clipped() -> None: _Client.response = _Response(payload={"issues": [{**ISSUE, "message": "x " * 500}]}) issues, _ = module.fetch_issues(CFG, "ariadne") assert len(issues[0]["message"]) <= 400 def test_a_missing_base_url_makes_no_request() -> None: issues, error = module.fetch_issues({**CFG, "sonar_base_url": ""}, "ariadne") assert issues == [] assert error == "sonar base url is empty" def test_a_missing_token_makes_no_request() -> None: issues, error = module.fetch_issues({**CFG, "sonar_token": ""}, "ariadne") assert issues == [] assert error == "sonar token is empty" def test_a_transport_failure_is_reported_not_raised() -> None: _Client.error = httpx.ConnectError("refused") issues, error = module.fetch_issues(CFG, "ariadne") assert issues == [] assert "sonar fetch failed" in error def test_a_non_200_is_reported() -> None: _Client.response = _Response(status_code=503) issues, error = module.fetch_issues(CFG, "ariadne") assert issues == [] assert error == "sonar fetch http 503" def test_an_unparseable_body_is_reported() -> None: _Client.response = _Response(raises=True) issues, error = module.fetch_issues(CFG, "ariadne") assert issues == [] assert "sonar response not json" in error def test_a_payload_that_is_not_a_dict_yields_nothing() -> None: _Client.response = _Response(payload=["nope"]) assert module.fetch_issues(CFG, "ariadne") == ([], None) def test_the_timeout_falls_back_when_unparseable() -> None: assert module._timeout({"timeout_seconds": "soon"}) == module._DEFAULT_TIMEOUT_SECONDS assert module._timeout({}) == module._DEFAULT_TIMEOUT_SECONDS @pytest.mark.parametrize( ("component", "expected"), [("proj:src/a.py", "src/a.py"), ("proj", ""), (None, ""), ("proj: a.py ", "a.py")], ) def test_the_component_key_yields_the_repository_path(component, expected) -> None: assert module.component_path(component) == expected