"""Shared pytest fixtures for the integration suite. These tests run against a deployed environment (BASE_URL, default QA). The fixtures here provide a reusable HTTP session and small helpers so individual test modules stay focused on the endpoint behaviour rather than transport. """ from os import getenv import pytest import requests # Default per-request timeout (seconds); overridable for slower environments. REQUEST_TIMEOUT = float(getenv("REQUEST_TIMEOUT", "30")) # HTTP verbs that can change server state. A *successful* call with one of # these from a test that is not marked `write` means data was mutated under the # supposedly-safe suite — see _guard_write_marker below. _MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"} @pytest.fixture(scope="session") def http(): """A shared requests.Session with a default timeout. Reusing one session keeps TCP connections warm across the suite and gives us a single place to attach default headers/timeouts later if needed. It also records mutating calls so the per-test guard can enforce the safe/write marker split (see _guard_write_marker). """ class _Session(requests.Session): def __init__(self): super().__init__() # (method, status_code) for mutating calls made by the current # test; cleared per-test by the _guard_write_marker fixture. self.mutations = [] def request(self, method, url, **kwargs): kwargs.setdefault("timeout", REQUEST_TIMEOUT) response = super().request(method, url, **kwargs) if method.upper() in _MUTATING_METHODS: self.mutations.append((method.upper(), response.status_code)) return response session = _Session() yield session session.close() @pytest.fixture(autouse=True) def _guard_write_marker(request, http): """Structurally enforce the safe/write split the markers only imply. A test that issues a *successful* (2xx) POST/PUT/PATCH/DELETE has mutated shared state and must carry ``@pytest.mark.write`` (so ``-m "not write"`` and CI skip it). Negative validation tests legitimately issue mutating verbs but receive 4xx (no mutation), so only a 2xx from an unmarked test is flagged — closing the gap where a new write test forgets its marker and silently runs against shared QA. """ http.mutations.clear() yield if request.node.get_closest_marker("write"): return succeeded = [(m, s) for (m, s) in http.mutations if 200 <= s < 300] assert not succeeded, ( "{} made a successful mutating request {} but is not marked " "@pytest.mark.write. It would run under the 'safe' suite " "(`-m \"not write\"`) against shared QA. Add the write marker and " "ensure the test cleans up after itself.".format( request.node.nodeid, succeeded))