From bf4ce3fd2212aaccb50f3594ac6154fef6855875 Mon Sep 17 00:00:00 2001 From: Mehdi ABAAKOUK Date: Tue, 5 May 2026 15:37:40 +0200 Subject: [PATCH 1/2] test(func-tests): add functional + live smoke harness for ci API commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new test layers parallel to compat-tests: - Mock-based functional tests under func-tests/ that drive the real mergify binary against pytest-httpserver and assert request / response shape for `ci scopes-send` (4 cases) and `ci junit-process` (5 cases). Wired into ci-gate via a new `func-tests` job in ci.yaml. - Live smoke tests (test_live_smoke.py, marked `pytest.mark.live`) that hit the real Mergify API at mergify-clients-testing/mergify-cli-repo PR #1. Skipped unless LIVE_TEST_MERGIFY_TOKEN is set; driven by a dedicated func-tests-live workflow on a nightly cron + manual dispatch, NOT wired into ci-gate. Catches the class of mock drift schema-level checks miss — real auth, real serialization, response shape. The mock layer keeps the PR feedback loop fast and offline-safe; the live layer is a canary for "the mock has lied about the wire contract." The live workflow runs in a dedicated `func-tests-live` GitHub Environment so the staging-tenant secret can be rotated and audited independently. Adds pytest-httpserver==1.1.3 to dev deps and a `live` pytest marker. Co-Authored-By: Claude Opus 4.7 (1M context) Change-Id: I1ee94f6c9e1d6ac7d4ad22fe07b98860ecec12e9 --- .github/workflows/ci.yaml | 25 +++ .github/workflows/func-tests-live.yaml | 41 +++++ func-tests/README.md | 70 ++++++++ func-tests/conftest.py | 195 ++++++++++++++++++++++ func-tests/fixtures/junit_fail.xml | 15 ++ func-tests/fixtures/junit_pass.xml | 8 + func-tests/test_junit_process.py | 220 +++++++++++++++++++++++++ func-tests/test_live_smoke.py | 80 +++++++++ func-tests/test_scopes_send.py | 149 +++++++++++++++++ poe.toml | 8 + pyproject.toml | 12 ++ uv.lock | 78 +++++++++ 12 files changed, 901 insertions(+) create mode 100644 .github/workflows/func-tests-live.yaml create mode 100644 func-tests/README.md create mode 100644 func-tests/conftest.py create mode 100644 func-tests/fixtures/junit_fail.xml create mode 100644 func-tests/fixtures/junit_pass.xml create mode 100644 func-tests/test_junit_process.py create mode 100644 func-tests/test_live_smoke.py create mode 100644 func-tests/test_scopes_send.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b56f595b..eaec0cfc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,6 +58,30 @@ jobs: shell: bash run: uv run --locked poe compat-test + func-tests: + timeout-minutes: 10 + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-python@v6.2.0 + with: + python-version: 3.14 + + - name: Install Rust toolchain + run: | + rustup toolchain install stable --profile minimal + rustup default stable + - uses: Swatinem/rust-cache@v2 + + - uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + version-file: requirements-uv.txt + + - name: Functional tests + shell: bash + run: uv run --locked poe func-test + rust: timeout-minutes: 10 runs-on: ubuntu-24.04 @@ -156,6 +180,7 @@ jobs: - test - linters - compat-tests + - func-tests - rust - wheels runs-on: ubuntu-latest diff --git a/.github/workflows/func-tests-live.yaml b/.github/workflows/func-tests-live.yaml new file mode 100644 index 00000000..d7704f8d --- /dev/null +++ b/.github/workflows/func-tests-live.yaml @@ -0,0 +1,41 @@ +name: Live functional tests + +# Hits the real Mergify API against +# mergify-clients-testing/mergify-cli-repo PR #1. NOT wired into +# the PR `ci-gate` job — an upstream blip must not block PRs. +# See `func-tests/test_live_smoke.py`. + +permissions: read-all + +on: + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + +jobs: + live-tests: + timeout-minutes: 10 + runs-on: ubuntu-24.04 + environment: func-tests-live + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-python@v6.2.0 + with: + python-version: 3.14 + + - name: Install Rust toolchain + run: | + rustup toolchain install stable --profile minimal + rustup default stable + - uses: Swatinem/rust-cache@v2 + + - uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + version-file: requirements-uv.txt + + - name: Live smoke tests + shell: bash + env: + LIVE_TEST_MERGIFY_TOKEN: ${{ secrets.MERGIFY_CLI_LIVE_TEST_MERGIFY_TOKEN }} + run: uv run --locked poe live-test diff --git a/func-tests/README.md b/func-tests/README.md new file mode 100644 index 00000000..5ace2e9a --- /dev/null +++ b/func-tests/README.md @@ -0,0 +1,70 @@ +# Functional tests + +End-to-end tests that drive the real `mergify` binary against a local +mock HTTP server (`pytest-httpserver`). Unlike `compat-tests/` — which +exercise CLI argument parsing and exit-code contracts that fire before +any external call — functional tests cover commands that talk to the +Mergify API: + +- `mergify ci scopes-send` — `POST /v1/repos/{owner}/{repo}/pulls/{n}/scopes` +- `mergify ci junit-process` — OTLP traces upload + quarantine check +- `mergify ci junit-upload` (deprecated alias of `junit-process`) + +Runner: `func-tests/test_*.py` (pytest-discovered). + +Invoke: + +```bash +uv run poe func-test +# or: uv run pytest func-tests/ +``` + +## How it works + +Each test: + +1. Starts a `pytest-httpserver` instance (real socket on `127.0.0.1`). +2. Registers expected request handlers (path, headers, body). +3. Invokes the `mergify` CLI as a subprocess pointed at that server + via `--api-url` / `MERGIFY_API_URL`. +4. Asserts the subprocess exit code, optional stdout substrings, and + that the mock received the expected request(s). + +The `mergify` binary is the user-facing entry point — it dispatches +to ported Rust subcommands or shells back to `python -m mergify_cli` +for the rest. Running through the binary tests the real release +artifact end-to-end. + +## Adding a test + +- Drop a JUnit XML fixture under `fixtures/` if the test needs one. +- Use the `httpserver` fixture (provided by `pytest-httpserver`) to + register expected requests and the `cli` fixture to invoke the + binary with the right env scrubbed. +- Assert on `result.returncode`, request count via `httpserver`, and + any user-visible stdout/stderr. + +## Live smoke tests + +`test_live_smoke.py` (marked `pytest.mark.live`) hits the real +Mergify API at `mergify-clients-testing/mergify-cli-repo` PR #1. +Skipped by default — runs in `.github/workflows/func-tests-live.yaml` +on a nightly cron and manual dispatch, never on PRs. + +Run locally: + +```bash +LIVE_TEST_MERGIFY_TOKEN= uv run poe live-test +``` + +The mock-vs-live split exists because the mock alone can drift +silently from the real API. The live job is a canary: when it +fails, the mock contract has gotten out of sync. + +## Why a real HTTP server (not respx) + +`respx`/`responses` patch the in-process HTTP client. Functional +tests run the CLI in a subprocess (often a Rust binary), so the mock +must be reachable over a real socket — `pytest-httpserver` runs a +threaded HTTP server bound to a random localhost port, which works +for both Python and Rust callers. diff --git a/func-tests/conftest.py b/func-tests/conftest.py new file mode 100644 index 00000000..131c2f4c --- /dev/null +++ b/func-tests/conftest.py @@ -0,0 +1,195 @@ +# +# Copyright © 2021-2026 Mergify SAS +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Shared fixtures for the functional-test harness. + +Tests in this directory run the `mergify` CLI as a subprocess +against a local mock HTTP server. The `cli` fixture builds the +invocation with a clean environment (CI/GitHub/Buildkite vars +scrubbed) so test runs are deterministic regardless of where they +execute. +""" + +from __future__ import annotations + +import dataclasses +import os +import pathlib +import shutil +import subprocess +import sys +import typing + + +if typing.TYPE_CHECKING: + from collections.abc import Mapping + from collections.abc import Sequence + +import pytest + + +FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" + +# Environment variables that the CLI auto-detects from the surrounding +# CI runner. Scrub them so a developer running tests inside GitHub +# Actions / Buildkite doesn't get different behavior than a clean +# laptop run. +_CI_ENV_VARS = ( + "CI", + "GITHUB_ACTIONS", + "GITHUB_REPOSITORY", + "GITHUB_REF", + "GITHUB_HEAD_REF", + "GITHUB_BASE_REF", + "GITHUB_EVENT_PATH", + "GITHUB_EVENT_NAME", + "GITHUB_OUTPUT", + "GITHUB_STEP_SUMMARY", + "GITHUB_TOKEN", + "BUILDKITE", + "BUILDKITE_PULL_REQUEST", + "BUILDKITE_PULL_REQUEST_BASE_BRANCH", + "BUILDKITE_BRANCH", + "BUILDKITE_COMMIT", + "MERGIFY_API_URL", + "MERGIFY_TOKEN", + "MERGIFY_CONFIG_PATH", + "MERGIFY_TEST_EXIT_CODE", + "ACTIONS_STEP_DEBUG", +) + + +@dataclasses.dataclass(frozen=True) +class CliResult: + returncode: int + stdout: str + stderr: str + + +@pytest.fixture +def live_token() -> str: + """Skip the live test if `LIVE_TEST_MERGIFY_TOKEN` isn't set.""" + token = os.environ.get("LIVE_TEST_MERGIFY_TOKEN", "").strip() + if not token: + pytest.skip("LIVE_TEST_MERGIFY_TOKEN unset") + return token + + +def _resolve_mergify_binary() -> pathlib.Path | None: + """Locate the `mergify` Rust binary in the active venv (or PATH). + + The binary is installed as a console-script entry point when the + project is `uv sync`'d. Tests for Rust-only subcommands + (`config validate`, `config simulate`, `ci scopes-send`) need + this binary; tests for still-Python subcommands can use either. + """ + venv = os.environ.get("VIRTUAL_ENV") + if venv: + candidate = pathlib.Path(venv) / "bin" / "mergify" + if candidate.exists(): + return candidate + candidate = pathlib.Path(venv) / "Scripts" / "mergify.exe" + if candidate.exists(): + return candidate + found = shutil.which("mergify") + return pathlib.Path(found) if found else None + + +@pytest.fixture(scope="session") +def mergify_binary() -> pathlib.Path: + binary = _resolve_mergify_binary() + if binary is None: + pytest.skip( + "`mergify` binary not found; run `uv sync` to install it", + ) + return binary + + +@pytest.fixture +def cli( + tmp_path: pathlib.Path, + mergify_binary: pathlib.Path, +) -> typing.Callable[..., CliResult]: + """Return a callable that runs `mergify ` in a subprocess. + + Routes through the real Rust binary so both native subcommands + (`ci scopes-send`) and shim-dispatched ones (`ci junit-process`) + are exercised end-to-end. Runs from a fresh temp directory with + CI-detection env vars scrubbed. Stdin is closed so any accidental + interactive prompt fails fast instead of blocking. A 30s timeout + caps pathological hangs — functional cases are short by design. + """ + + def _run( + *args: str, + env: Mapping[str, str] | None = None, + cwd: pathlib.Path | None = None, + ) -> CliResult: + full_env = {k: v for k, v in os.environ.items() if k not in _CI_ENV_VARS} + if env: + full_env.update(env) + + cmd: Sequence[str] = [str(mergify_binary), *args] + return _exec(cmd, env=full_env, cwd=cwd or tmp_path) + + return _run + + +@pytest.fixture +def cli_py( + tmp_path: pathlib.Path, +) -> typing.Callable[..., CliResult]: + """Like `cli`, but invokes `python -m mergify_cli` directly. + + Useful for tests targeting subcommands that only exist in the + Python implementation (none today, but kept for symmetry with + `compat-tests`). + """ + + def _run( + *args: str, + env: Mapping[str, str] | None = None, + cwd: pathlib.Path | None = None, + ) -> CliResult: + full_env = {k: v for k, v in os.environ.items() if k not in _CI_ENV_VARS} + if env: + full_env.update(env) + + cmd: Sequence[str] = [sys.executable, "-m", "mergify_cli", *args] + return _exec(cmd, env=full_env, cwd=cwd or tmp_path) + + return _run + + +def _exec( + cmd: Sequence[str], + *, + env: Mapping[str, str], + cwd: pathlib.Path, +) -> CliResult: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + stdin=subprocess.DEVNULL, + env=dict(env), + cwd=str(cwd), + timeout=30, + ) + return CliResult( + returncode=proc.returncode, + stdout=proc.stdout, + stderr=proc.stderr, + ) diff --git a/func-tests/fixtures/junit_fail.xml b/func-tests/fixtures/junit_fail.xml new file mode 100644 index 00000000..598ffdff --- /dev/null +++ b/func-tests/fixtures/junit_fail.xml @@ -0,0 +1,15 @@ + + + + + + def test_failed() -> None: + > assert 1 == 0 + E assert 1 == 0 + + tests/test_func.py:6: AssertionError + + + + diff --git a/func-tests/fixtures/junit_pass.xml b/func-tests/fixtures/junit_pass.xml new file mode 100644 index 00000000..39fdfd96 --- /dev/null +++ b/func-tests/fixtures/junit_pass.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/func-tests/test_junit_process.py b/func-tests/test_junit_process.py new file mode 100644 index 00000000..88c8ec25 --- /dev/null +++ b/func-tests/test_junit_process.py @@ -0,0 +1,220 @@ +# +# Copyright © 2021-2026 Mergify SAS +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Functional tests for `mergify ci junit-process` (and the +deprecated `junit-upload` alias). + +These commands hit two Mergify endpoints: + +- ``POST /v1/ci/{owner}/repositories/{repo}/quarantines/check`` — + asks whether failing tests are quarantined. +- ``POST /v1/repos/{owner}/{repo}/ci/traces`` — OTLP traces upload + for the JUnit results. + +We assert the quarantine endpoint precisely (URL, headers, JSON +body). The OTLP traces upload is asserted only by method + auth +header + non-empty body — its payload is gzip-compressed protobuf, +which is painful to introspect from a black-box test and not the +contract we care about here. +""" + +from __future__ import annotations + +import pathlib +import typing + + +if typing.TYPE_CHECKING: + from pytest_httpserver import HTTPServer + + +FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" +JUNIT_PASS = FIXTURES_DIR / "junit_pass.xml" +JUNIT_FAIL = FIXTURES_DIR / "junit_fail.xml" + + +def _expect_quarantine_check( + httpserver: HTTPServer, + *, + response: dict[str, list[str]], + status: int = 200, +) -> None: + httpserver.expect_request( + "/v1/ci/owner/repositories/repo/quarantines/check", + method="POST", + headers={"Authorization": "Bearer test-token"}, + ).respond_with_json(response, status=status) + + +def _expect_traces_upload(httpserver: HTTPServer) -> None: + httpserver.expect_request( + "/v1/repos/owner/repo/ci/traces", + method="POST", + headers={"Authorization": "Bearer test-token"}, + ).respond_with_data("", status=200) + + +def test_junit_process_all_passing( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + """No failures → quarantine still queried (with empty list), traces uploaded, exit 0.""" + _expect_quarantine_check( + httpserver, + response={"quarantined_tests_names": [], "non_quarantined_tests_names": []}, + ) + _expect_traces_upload(httpserver) + + result = cli( + "ci", + "junit-process", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "test-token", + "--repository", + "owner/repo", + "--tests-target-branch", + "main", + str(JUNIT_PASS), + ) + + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + assert "OK" in result.stdout + # Traces endpoint must have been hit. + assert any( + req.path == "/v1/repos/owner/repo/ci/traces" for req, _ in httpserver.log + ), "OTLP traces endpoint was not called" + + +def test_junit_process_failure_quarantined( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + """Failing test that *is* quarantined → exit 0, FAIL message absent.""" + _expect_quarantine_check( + httpserver, + response={ + "quarantined_tests_names": ["tests.test_func.test_failed"], + "non_quarantined_tests_names": [], + }, + ) + _expect_traces_upload(httpserver) + + result = cli( + "ci", + "junit-process", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "test-token", + "--repository", + "owner/repo", + "--tests-target-branch", + "main", + str(JUNIT_FAIL), + ) + + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + assert "quarantined" in result.stdout.lower() + + +def test_junit_process_failure_not_quarantined( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + """Failing test, not quarantined → exit 1.""" + _expect_quarantine_check( + httpserver, + response={ + "quarantined_tests_names": [], + "non_quarantined_tests_names": ["tests.test_func.test_failed"], + }, + ) + _expect_traces_upload(httpserver) + + result = cli( + "ci", + "junit-process", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "test-token", + "--repository", + "owner/repo", + "--tests-target-branch", + "main", + str(JUNIT_FAIL), + ) + + assert result.returncode == 1, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + assert "FAIL" in result.stdout + + +def test_junit_process_quarantine_endpoint_error( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + """Quarantine endpoint 500 → failures treated as blocking, exit 1.""" + _expect_quarantine_check( + httpserver, + response={"quarantined_tests_names": [], "non_quarantined_tests_names": []}, + status=500, + ) + _expect_traces_upload(httpserver) + + result = cli( + "ci", + "junit-process", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "test-token", + "--repository", + "owner/repo", + "--tests-target-branch", + "main", + str(JUNIT_FAIL), + ) + + assert result.returncode == 1 + assert "Failed to check quarantine" in result.stdout + + +def test_junit_upload_alias_still_works( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + """Deprecated `junit-upload` is a shim over `junit-process`.""" + _expect_quarantine_check( + httpserver, + response={"quarantined_tests_names": [], "non_quarantined_tests_names": []}, + ) + _expect_traces_upload(httpserver) + + result = cli( + "ci", + "junit-upload", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "test-token", + "--repository", + "owner/repo", + "--tests-target-branch", + "main", + str(JUNIT_PASS), + ) + + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" diff --git a/func-tests/test_live_smoke.py b/func-tests/test_live_smoke.py new file mode 100644 index 00000000..5707b6b4 --- /dev/null +++ b/func-tests/test_live_smoke.py @@ -0,0 +1,80 @@ +# +# Copyright © 2021-2026 Mergify SAS +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Live smoke tests against the real Mergify API. + +Run nightly via `func-tests-live.yaml` against +`mergify-clients-testing/mergify-cli-repo` PR #1. Catches the +class of mock drift that schema-level checks miss: real auth +behavior, real serialization, response shapes the mock pretends +about. Skipped unless `LIVE_TEST_MERGIFY_TOKEN` is set. +""" + +from __future__ import annotations + +import pathlib +import typing + +import pytest + + +pytestmark = pytest.mark.live + + +API_URL = "https://api.mergify.com" +REPOSITORY = "mergify-clients-testing/mergify-cli-repo" +PULL_REQUEST = 1 + +JUNIT_PASS = pathlib.Path(__file__).parent / "fixtures" / "junit_pass.xml" + + +def test_scopes_send_against_real_api( + live_token: str, + cli: typing.Callable[..., typing.Any], +) -> None: + result = cli( + "ci", + "scopes-send", + "--api-url", + API_URL, + "--token", + live_token, + "--repository", + REPOSITORY, + "--pull-request", + str(PULL_REQUEST), + "--scope", + "func-tests-live-smoke", + ) + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + + +def test_junit_process_against_real_api( + live_token: str, + cli: typing.Callable[..., typing.Any], +) -> None: + result = cli( + "ci", + "junit-process", + "--api-url", + API_URL, + "--token", + live_token, + "--repository", + REPOSITORY, + "--tests-target-branch", + "main", + str(JUNIT_PASS), + ) + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" diff --git a/func-tests/test_scopes_send.py b/func-tests/test_scopes_send.py new file mode 100644 index 00000000..ebdb171d --- /dev/null +++ b/func-tests/test_scopes_send.py @@ -0,0 +1,149 @@ +# +# Copyright © 2021-2026 Mergify SAS +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Functional tests for `mergify ci scopes-send`.""" + +from __future__ import annotations + +import json +import typing + + +if typing.TYPE_CHECKING: + import pathlib + + from pytest_httpserver import HTTPServer + + +def test_scopes_send_posts_direct_scopes( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + httpserver.expect_oneshot_request( + "/v1/repos/owner/repo/pulls/42/scopes", + method="POST", + headers={"Authorization": "Bearer test-token"}, + json={"scopes": ["backend"]}, + ).respond_with_data("", status=200) + + result = cli( + "ci", + "scopes-send", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "test-token", + "--repository", + "owner/repo", + "--pull-request", + "42", + "--scope", + "backend", + ) + + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + httpserver.check_assertions() + + +def test_scopes_send_combines_flags_json_and_text_file( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], + tmp_path: pathlib.Path, +) -> None: + json_path = tmp_path / "scopes.json" + json_path.write_text(json.dumps({"scopes": ["fromjson"]})) + + txt_path = tmp_path / "scopes.txt" + txt_path.write_text("fromtext\n") + + httpserver.expect_oneshot_request( + "/v1/repos/owner/repo/pulls/7/scopes", + method="POST", + json={"scopes": ["direct", "fromjson", "fromtext"]}, + ).respond_with_data("", status=200) + + result = cli( + "ci", + "scopes-send", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "t", + "--repository", + "owner/repo", + "--pull-request", + "7", + "--scope", + "direct", + "--scopes-json", + str(json_path), + "--scopes-file", + str(txt_path), + ) + + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + httpserver.check_assertions() + + +def test_scopes_send_skips_when_no_pull_request( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + """No PR detected (no flag, no GITHUB_EVENT_PATH) → clean skip, no HTTP.""" + result = cli( + "ci", + "scopes-send", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "t", + "--repository", + "owner/repo", + "--scope", + "backend", + ) + + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + # No request should have been made. + assert len(httpserver.log) == 0, ( + f"unexpected requests: {[r[0].path for r in httpserver.log]}" + ) + + +def test_scopes_send_propagates_server_error( + httpserver: HTTPServer, + cli: typing.Callable[..., typing.Any], +) -> None: + httpserver.expect_oneshot_request( + "/v1/repos/owner/repo/pulls/1/scopes", + method="POST", + ).respond_with_data("forbidden", status=403) + + result = cli( + "ci", + "scopes-send", + "--api-url", + httpserver.url_for("").rstrip("/"), + "--token", + "t", + "--repository", + "owner/repo", + "--pull-request", + "1", + "--scope", + "backend", + ) + + assert result.returncode != 0 + httpserver.check_assertions() diff --git a/poe.toml b/poe.toml index 32ed624e..f2dc67de 100644 --- a/poe.toml +++ b/poe.toml @@ -8,6 +8,14 @@ cmd = "pytest -v --pyargs mergify_cli" help = "Run cross-implementation compat tests (see compat-tests/README.md)" cmd = "pytest -v compat-tests/" +[tool.poe.tasks.func-test] +help = "Run functional tests against a mock Mergify API (see func-tests/README.md)" +cmd = "pytest -v func-tests/ -m \"not live\"" + +[tool.poe.tasks.live-test] +help = "Run live smoke tests against the real Mergify staging API (requires LIVE_TEST_MERGIFY_TOKEN)" +cmd = "pytest -v func-tests/ -m live" + [tool.poe.tasks.linters] help = "Run linters" default_item_type = "cmd" diff --git a/pyproject.toml b/pyproject.toml index f96ce860..027bd54e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ dev = [ "types-pyyaml==6.0.12.20260408", "anys==0.3.1", "responses==0.26.0", + "pytest-httpserver==1.1.3", ] [build-system] @@ -91,6 +92,9 @@ include = ["LICENSE"] [tool.pytest.ini_options] asyncio_mode = "auto" +markers = [ + "live: hits the real Mergify staging API. Skipped unless LIVE_TEST_MERGIFY_TOKEN is set; runs in the dedicated `func-tests-live` workflow only.", +] [tool.poe] include = ["poe.toml"] @@ -252,6 +256,14 @@ runtime-evaluated-decorators = [ # subprocess call: check for execution of untrusted input "S603", ] +"func-tests/**/*.py" = [ + # Use of assert detected — this is pytest + "S101", + # subprocess call: check for execution of untrusted input + "S603", + # hardcoded passwords / tokens — fixtures only, not real creds + "S105", "S106", +] [tool.ruff.lint.isort] force-single-line = true diff --git a/uv.lock b/uv.lock index 936b4484..5ac1fff9 100644 --- a/uv.lock +++ b/uv.lock @@ -272,6 +272,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -308,6 +360,7 @@ dev = [ { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-httpserver" }, { name = "responses" }, { name = "respx" }, { name = "ruff" }, @@ -341,6 +394,7 @@ dev = [ { name = "poethepoet", specifier = "==0.45.0" }, { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, + { name = "pytest-httpserver", specifier = "==1.1.3" }, { name = "responses", specifier = "==0.26.0" }, { name = "respx", specifier = "==0.23.1" }, { name = "ruff", specifier = "==0.15.12" }, @@ -660,6 +714,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -983,6 +1049,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + [[package]] name = "zipp" version = "3.21.0" From 69d6c1bb0edfc63bbdaa9a5828418cbc3708cfe2 Mon Sep 17 00:00:00 2001 From: Mehdi ABAAKOUK Date: Tue, 5 May 2026 15:42:42 +0200 Subject: [PATCH 2/2] test(func-tests): drop mock layer, keep live smoke + add config simulate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mock layer added in the previous commit duplicated coverage already provided by the in-tree Rust wiremock unit tests; keeping it added a pytest-httpserver dependency and a parallel PR-gate job for no incremental signal. Removed: - test_scopes_send.py / test_junit_process.py (mock-based) - fixtures/junit_fail.xml (only used by deleted tests) - pytest-httpserver dev dep - `func-test` poe task and the `func-tests` PR-gate job Added: - test_config_simulate — third live smoke test covering POST /v1/repos/{owner}/{repo}/pulls/{n}/simulator func-tests/ is now live-only: three smoke tests gated on LIVE_TEST_MERGIFY_TOKEN, run from the dedicated nightly workflow. Co-Authored-By: Claude Opus 4.7 (1M context) Change-Id: Ia8144945e0c4f1227b075c8614aa5dfa5dc1c3c1 --- .github/workflows/ci.yaml | 25 ---- func-tests/README.md | 78 +++------- func-tests/conftest.py | 97 ++++--------- func-tests/fixtures/junit_fail.xml | 15 -- func-tests/test_junit_process.py | 220 ----------------------------- func-tests/test_live_smoke.py | 36 ++++- func-tests/test_scopes_send.py | 149 ------------------- poe.toml | 6 +- pyproject.toml | 3 +- uv.lock | 78 ---------- 10 files changed, 80 insertions(+), 627 deletions(-) delete mode 100644 func-tests/fixtures/junit_fail.xml delete mode 100644 func-tests/test_junit_process.py delete mode 100644 func-tests/test_scopes_send.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eaec0cfc..b56f595b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,30 +58,6 @@ jobs: shell: bash run: uv run --locked poe compat-test - func-tests: - timeout-minutes: 10 - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6.0.2 - - uses: actions/setup-python@v6.2.0 - with: - python-version: 3.14 - - - name: Install Rust toolchain - run: | - rustup toolchain install stable --profile minimal - rustup default stable - - uses: Swatinem/rust-cache@v2 - - - uses: astral-sh/setup-uv@v8.1.0 - with: - enable-cache: true - version-file: requirements-uv.txt - - - name: Functional tests - shell: bash - run: uv run --locked poe func-test - rust: timeout-minutes: 10 runs-on: ubuntu-24.04 @@ -180,7 +156,6 @@ jobs: - test - linters - compat-tests - - func-tests - rust - wheels runs-on: ubuntu-latest diff --git a/func-tests/README.md b/func-tests/README.md index 5ace2e9a..08c36244 100644 --- a/func-tests/README.md +++ b/func-tests/README.md @@ -1,70 +1,38 @@ -# Functional tests +# Live functional tests -End-to-end tests that drive the real `mergify` binary against a local -mock HTTP server (`pytest-httpserver`). Unlike `compat-tests/` — which -exercise CLI argument parsing and exit-code contracts that fire before -any external call — functional tests cover commands that talk to the -Mergify API: +End-to-end smoke tests that drive the real `mergify` binary +against the real Mergify API at +`mergify-clients-testing/mergify-cli-repo` PR #1. +Coverage: + +- `mergify config simulate` — `POST /v1/repos/{owner}/{repo}/pulls/{n}/simulator` - `mergify ci scopes-send` — `POST /v1/repos/{owner}/{repo}/pulls/{n}/scopes` - `mergify ci junit-process` — OTLP traces upload + quarantine check -- `mergify ci junit-upload` (deprecated alias of `junit-process`) - -Runner: `func-tests/test_*.py` (pytest-discovered). -Invoke: +Each test fires when the real API's URL, auth, or wire format +diverges from what the CLI expects. Asserts only "endpoint exists, +accepts our payload, returns 2xx" — never response content, since +the test tenant's state is not under test control. -```bash -uv run poe func-test -# or: uv run pytest func-tests/ -``` +## Running -## How it works +CI: `.github/workflows/func-tests-live.yaml` runs nightly + on +manual dispatch. Not wired into the PR `ci-gate`, so an upstream +blip cannot block PRs. -Each test: - -1. Starts a `pytest-httpserver` instance (real socket on `127.0.0.1`). -2. Registers expected request handlers (path, headers, body). -3. Invokes the `mergify` CLI as a subprocess pointed at that server - via `--api-url` / `MERGIFY_API_URL`. -4. Asserts the subprocess exit code, optional stdout substrings, and - that the mock received the expected request(s). - -The `mergify` binary is the user-facing entry point — it dispatches -to ported Rust subcommands or shells back to `python -m mergify_cli` -for the rest. Running through the binary tests the real release -artifact end-to-end. - -## Adding a test - -- Drop a JUnit XML fixture under `fixtures/` if the test needs one. -- Use the `httpserver` fixture (provided by `pytest-httpserver`) to - register expected requests and the `cli` fixture to invoke the - binary with the right env scrubbed. -- Assert on `result.returncode`, request count via `httpserver`, and - any user-visible stdout/stderr. - -## Live smoke tests - -`test_live_smoke.py` (marked `pytest.mark.live`) hits the real -Mergify API at `mergify-clients-testing/mergify-cli-repo` PR #1. -Skipped by default — runs in `.github/workflows/func-tests-live.yaml` -on a nightly cron and manual dispatch, never on PRs. - -Run locally: +Locally: ```bash LIVE_TEST_MERGIFY_TOKEN= uv run poe live-test ``` -The mock-vs-live split exists because the mock alone can drift -silently from the real API. The live job is a canary: when it -fails, the mock contract has gotten out of sync. +Skipped if `LIVE_TEST_MERGIFY_TOKEN` is unset. -## Why a real HTTP server (not respx) +## Adding a test -`respx`/`responses` patch the in-process HTTP client. Functional -tests run the CLI in a subprocess (often a Rust binary), so the mock -must be reachable over a real socket — `pytest-httpserver` runs a -threaded HTTP server bound to a random localhost port, which works -for both Python and Rust callers. +- Mark with `pytest.mark.live` (the module-level `pytestmark = pytest.mark.live` + in `test_live_smoke.py` already does this). +- Use the `cli` fixture to invoke the binary and `live_token` to + inject the token. +- Assert exit code only — never response content. diff --git a/func-tests/conftest.py b/func-tests/conftest.py index 131c2f4c..7435446f 100644 --- a/func-tests/conftest.py +++ b/func-tests/conftest.py @@ -12,13 +12,12 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -"""Shared fixtures for the functional-test harness. +"""Shared fixtures for the live functional-test harness. -Tests in this directory run the `mergify` CLI as a subprocess -against a local mock HTTP server. The `cli` fixture builds the -invocation with a clean environment (CI/GitHub/Buildkite vars -scrubbed) so test runs are deterministic regardless of where they -execute. +Tests in this directory drive the real `mergify` binary against +the real Mergify API. The `cli` fixture builds the invocation +with a clean environment (CI/GitHub/Buildkite vars scrubbed) so +runs are deterministic regardless of where they execute. """ from __future__ import annotations @@ -28,7 +27,6 @@ import pathlib import shutil import subprocess -import sys import typing @@ -39,8 +37,6 @@ import pytest -FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" - # Environment variables that the CLI auto-detects from the surrounding # CI runner. Scrub them so a developer running tests inside GitHub # Actions / Buildkite doesn't get different behavior than a clean @@ -87,13 +83,7 @@ def live_token() -> str: def _resolve_mergify_binary() -> pathlib.Path | None: - """Locate the `mergify` Rust binary in the active venv (or PATH). - - The binary is installed as a console-script entry point when the - project is `uv sync`'d. Tests for Rust-only subcommands - (`config validate`, `config simulate`, `ci scopes-send`) need - this binary; tests for still-Python subcommands can use either. - """ + """Locate the `mergify` binary in the active venv (or PATH).""" venv = os.environ.get("VIRTUAL_ENV") if venv: candidate = pathlib.Path(venv) / "bin" / "mergify" @@ -123,12 +113,10 @@ def cli( ) -> typing.Callable[..., CliResult]: """Return a callable that runs `mergify ` in a subprocess. - Routes through the real Rust binary so both native subcommands - (`ci scopes-send`) and shim-dispatched ones (`ci junit-process`) - are exercised end-to-end. Runs from a fresh temp directory with - CI-detection env vars scrubbed. Stdin is closed so any accidental - interactive prompt fails fast instead of blocking. A 30s timeout - caps pathological hangs — functional cases are short by design. + Runs from a fresh temp directory with CI-detection env vars + scrubbed. Stdin is closed so any accidental interactive prompt + fails fast instead of blocking. A 30s timeout caps pathological + hangs. """ def _run( @@ -141,55 +129,20 @@ def _run( full_env.update(env) cmd: Sequence[str] = [str(mergify_binary), *args] - return _exec(cmd, env=full_env, cwd=cwd or tmp_path) - - return _run - - -@pytest.fixture -def cli_py( - tmp_path: pathlib.Path, -) -> typing.Callable[..., CliResult]: - """Like `cli`, but invokes `python -m mergify_cli` directly. - - Useful for tests targeting subcommands that only exist in the - Python implementation (none today, but kept for symmetry with - `compat-tests`). - """ - - def _run( - *args: str, - env: Mapping[str, str] | None = None, - cwd: pathlib.Path | None = None, - ) -> CliResult: - full_env = {k: v for k, v in os.environ.items() if k not in _CI_ENV_VARS} - if env: - full_env.update(env) - - cmd: Sequence[str] = [sys.executable, "-m", "mergify_cli", *args] - return _exec(cmd, env=full_env, cwd=cwd or tmp_path) + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + stdin=subprocess.DEVNULL, + env=dict(full_env), + cwd=str(cwd or tmp_path), + timeout=30, + ) + return CliResult( + returncode=proc.returncode, + stdout=proc.stdout, + stderr=proc.stderr, + ) return _run - - -def _exec( - cmd: Sequence[str], - *, - env: Mapping[str, str], - cwd: pathlib.Path, -) -> CliResult: - proc = subprocess.run( - cmd, - capture_output=True, - text=True, - check=False, - stdin=subprocess.DEVNULL, - env=dict(env), - cwd=str(cwd), - timeout=30, - ) - return CliResult( - returncode=proc.returncode, - stdout=proc.stdout, - stderr=proc.stderr, - ) diff --git a/func-tests/fixtures/junit_fail.xml b/func-tests/fixtures/junit_fail.xml deleted file mode 100644 index 598ffdff..00000000 --- a/func-tests/fixtures/junit_fail.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - def test_failed() -> None: - > assert 1 == 0 - E assert 1 == 0 - - tests/test_func.py:6: AssertionError - - - - diff --git a/func-tests/test_junit_process.py b/func-tests/test_junit_process.py deleted file mode 100644 index 88c8ec25..00000000 --- a/func-tests/test_junit_process.py +++ /dev/null @@ -1,220 +0,0 @@ -# -# Copyright © 2021-2026 Mergify SAS -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Functional tests for `mergify ci junit-process` (and the -deprecated `junit-upload` alias). - -These commands hit two Mergify endpoints: - -- ``POST /v1/ci/{owner}/repositories/{repo}/quarantines/check`` — - asks whether failing tests are quarantined. -- ``POST /v1/repos/{owner}/{repo}/ci/traces`` — OTLP traces upload - for the JUnit results. - -We assert the quarantine endpoint precisely (URL, headers, JSON -body). The OTLP traces upload is asserted only by method + auth -header + non-empty body — its payload is gzip-compressed protobuf, -which is painful to introspect from a black-box test and not the -contract we care about here. -""" - -from __future__ import annotations - -import pathlib -import typing - - -if typing.TYPE_CHECKING: - from pytest_httpserver import HTTPServer - - -FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" -JUNIT_PASS = FIXTURES_DIR / "junit_pass.xml" -JUNIT_FAIL = FIXTURES_DIR / "junit_fail.xml" - - -def _expect_quarantine_check( - httpserver: HTTPServer, - *, - response: dict[str, list[str]], - status: int = 200, -) -> None: - httpserver.expect_request( - "/v1/ci/owner/repositories/repo/quarantines/check", - method="POST", - headers={"Authorization": "Bearer test-token"}, - ).respond_with_json(response, status=status) - - -def _expect_traces_upload(httpserver: HTTPServer) -> None: - httpserver.expect_request( - "/v1/repos/owner/repo/ci/traces", - method="POST", - headers={"Authorization": "Bearer test-token"}, - ).respond_with_data("", status=200) - - -def test_junit_process_all_passing( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - """No failures → quarantine still queried (with empty list), traces uploaded, exit 0.""" - _expect_quarantine_check( - httpserver, - response={"quarantined_tests_names": [], "non_quarantined_tests_names": []}, - ) - _expect_traces_upload(httpserver) - - result = cli( - "ci", - "junit-process", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "test-token", - "--repository", - "owner/repo", - "--tests-target-branch", - "main", - str(JUNIT_PASS), - ) - - assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - assert "OK" in result.stdout - # Traces endpoint must have been hit. - assert any( - req.path == "/v1/repos/owner/repo/ci/traces" for req, _ in httpserver.log - ), "OTLP traces endpoint was not called" - - -def test_junit_process_failure_quarantined( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - """Failing test that *is* quarantined → exit 0, FAIL message absent.""" - _expect_quarantine_check( - httpserver, - response={ - "quarantined_tests_names": ["tests.test_func.test_failed"], - "non_quarantined_tests_names": [], - }, - ) - _expect_traces_upload(httpserver) - - result = cli( - "ci", - "junit-process", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "test-token", - "--repository", - "owner/repo", - "--tests-target-branch", - "main", - str(JUNIT_FAIL), - ) - - assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - assert "quarantined" in result.stdout.lower() - - -def test_junit_process_failure_not_quarantined( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - """Failing test, not quarantined → exit 1.""" - _expect_quarantine_check( - httpserver, - response={ - "quarantined_tests_names": [], - "non_quarantined_tests_names": ["tests.test_func.test_failed"], - }, - ) - _expect_traces_upload(httpserver) - - result = cli( - "ci", - "junit-process", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "test-token", - "--repository", - "owner/repo", - "--tests-target-branch", - "main", - str(JUNIT_FAIL), - ) - - assert result.returncode == 1, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - assert "FAIL" in result.stdout - - -def test_junit_process_quarantine_endpoint_error( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - """Quarantine endpoint 500 → failures treated as blocking, exit 1.""" - _expect_quarantine_check( - httpserver, - response={"quarantined_tests_names": [], "non_quarantined_tests_names": []}, - status=500, - ) - _expect_traces_upload(httpserver) - - result = cli( - "ci", - "junit-process", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "test-token", - "--repository", - "owner/repo", - "--tests-target-branch", - "main", - str(JUNIT_FAIL), - ) - - assert result.returncode == 1 - assert "Failed to check quarantine" in result.stdout - - -def test_junit_upload_alias_still_works( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - """Deprecated `junit-upload` is a shim over `junit-process`.""" - _expect_quarantine_check( - httpserver, - response={"quarantined_tests_names": [], "non_quarantined_tests_names": []}, - ) - _expect_traces_upload(httpserver) - - result = cli( - "ci", - "junit-upload", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "test-token", - "--repository", - "owner/repo", - "--tests-target-branch", - "main", - str(JUNIT_PASS), - ) - - assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" diff --git a/func-tests/test_live_smoke.py b/func-tests/test_live_smoke.py index 5707b6b4..795dce1d 100644 --- a/func-tests/test_live_smoke.py +++ b/func-tests/test_live_smoke.py @@ -15,10 +15,9 @@ """Live smoke tests against the real Mergify API. Run nightly via `func-tests-live.yaml` against -`mergify-clients-testing/mergify-cli-repo` PR #1. Catches the -class of mock drift that schema-level checks miss: real auth -behavior, real serialization, response shapes the mock pretends -about. Skipped unless `LIVE_TEST_MERGIFY_TOKEN` is set. +`mergify-clients-testing/mergify-cli-repo` PR #1. Each test fires +when the real API's URL, auth, or wire format diverges from what +the CLI expects. Skipped unless `LIVE_TEST_MERGIFY_TOKEN` is set. """ from __future__ import annotations @@ -35,14 +34,16 @@ class of mock drift that schema-level checks miss: real auth API_URL = "https://api.mergify.com" REPOSITORY = "mergify-clients-testing/mergify-cli-repo" PULL_REQUEST = 1 +PULL_REQUEST_URL = f"https://github.com/{REPOSITORY}/pull/{PULL_REQUEST}" JUNIT_PASS = pathlib.Path(__file__).parent / "fixtures" / "junit_pass.xml" -def test_scopes_send_against_real_api( +def test_scopes_send( live_token: str, cli: typing.Callable[..., typing.Any], ) -> None: + """`POST /v1/repos/{owner}/{repo}/pulls/{n}/scopes`.""" result = cli( "ci", "scopes-send", @@ -60,10 +61,11 @@ def test_scopes_send_against_real_api( assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" -def test_junit_process_against_real_api( +def test_junit_process( live_token: str, cli: typing.Callable[..., typing.Any], ) -> None: + """OTLP traces upload + quarantine check round-trip.""" result = cli( "ci", "junit-process", @@ -78,3 +80,25 @@ def test_junit_process_against_real_api( str(JUNIT_PASS), ) assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + + +def test_config_simulate( + live_token: str, + cli: typing.Callable[..., typing.Any], + tmp_path: pathlib.Path, +) -> None: + """`POST /v1/repos/{owner}/{repo}/pulls/{n}/simulator`.""" + config = tmp_path / ".mergify.yml" + config.write_text("pull_request_rules: []\n") + result = cli( + "config", + "--config-file", + str(config), + "simulate", + "--api-url", + API_URL, + "--token", + live_token, + PULL_REQUEST_URL, + ) + assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" diff --git a/func-tests/test_scopes_send.py b/func-tests/test_scopes_send.py deleted file mode 100644 index ebdb171d..00000000 --- a/func-tests/test_scopes_send.py +++ /dev/null @@ -1,149 +0,0 @@ -# -# Copyright © 2021-2026 Mergify SAS -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Functional tests for `mergify ci scopes-send`.""" - -from __future__ import annotations - -import json -import typing - - -if typing.TYPE_CHECKING: - import pathlib - - from pytest_httpserver import HTTPServer - - -def test_scopes_send_posts_direct_scopes( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - httpserver.expect_oneshot_request( - "/v1/repos/owner/repo/pulls/42/scopes", - method="POST", - headers={"Authorization": "Bearer test-token"}, - json={"scopes": ["backend"]}, - ).respond_with_data("", status=200) - - result = cli( - "ci", - "scopes-send", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "test-token", - "--repository", - "owner/repo", - "--pull-request", - "42", - "--scope", - "backend", - ) - - assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - httpserver.check_assertions() - - -def test_scopes_send_combines_flags_json_and_text_file( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], - tmp_path: pathlib.Path, -) -> None: - json_path = tmp_path / "scopes.json" - json_path.write_text(json.dumps({"scopes": ["fromjson"]})) - - txt_path = tmp_path / "scopes.txt" - txt_path.write_text("fromtext\n") - - httpserver.expect_oneshot_request( - "/v1/repos/owner/repo/pulls/7/scopes", - method="POST", - json={"scopes": ["direct", "fromjson", "fromtext"]}, - ).respond_with_data("", status=200) - - result = cli( - "ci", - "scopes-send", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "t", - "--repository", - "owner/repo", - "--pull-request", - "7", - "--scope", - "direct", - "--scopes-json", - str(json_path), - "--scopes-file", - str(txt_path), - ) - - assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - httpserver.check_assertions() - - -def test_scopes_send_skips_when_no_pull_request( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - """No PR detected (no flag, no GITHUB_EVENT_PATH) → clean skip, no HTTP.""" - result = cli( - "ci", - "scopes-send", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "t", - "--repository", - "owner/repo", - "--scope", - "backend", - ) - - assert result.returncode == 0, f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - # No request should have been made. - assert len(httpserver.log) == 0, ( - f"unexpected requests: {[r[0].path for r in httpserver.log]}" - ) - - -def test_scopes_send_propagates_server_error( - httpserver: HTTPServer, - cli: typing.Callable[..., typing.Any], -) -> None: - httpserver.expect_oneshot_request( - "/v1/repos/owner/repo/pulls/1/scopes", - method="POST", - ).respond_with_data("forbidden", status=403) - - result = cli( - "ci", - "scopes-send", - "--api-url", - httpserver.url_for("").rstrip("/"), - "--token", - "t", - "--repository", - "owner/repo", - "--pull-request", - "1", - "--scope", - "backend", - ) - - assert result.returncode != 0 - httpserver.check_assertions() diff --git a/poe.toml b/poe.toml index f2dc67de..ab646656 100644 --- a/poe.toml +++ b/poe.toml @@ -8,12 +8,8 @@ cmd = "pytest -v --pyargs mergify_cli" help = "Run cross-implementation compat tests (see compat-tests/README.md)" cmd = "pytest -v compat-tests/" -[tool.poe.tasks.func-test] -help = "Run functional tests against a mock Mergify API (see func-tests/README.md)" -cmd = "pytest -v func-tests/ -m \"not live\"" - [tool.poe.tasks.live-test] -help = "Run live smoke tests against the real Mergify staging API (requires LIVE_TEST_MERGIFY_TOKEN)" +help = "Run live smoke tests against the real Mergify API (requires LIVE_TEST_MERGIFY_TOKEN)" cmd = "pytest -v func-tests/ -m live" [tool.poe.tasks.linters] diff --git a/pyproject.toml b/pyproject.toml index 027bd54e..a4df9e43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ dev = [ "types-pyyaml==6.0.12.20260408", "anys==0.3.1", "responses==0.26.0", - "pytest-httpserver==1.1.3", ] [build-system] @@ -93,7 +92,7 @@ include = ["LICENSE"] [tool.pytest.ini_options] asyncio_mode = "auto" markers = [ - "live: hits the real Mergify staging API. Skipped unless LIVE_TEST_MERGIFY_TOKEN is set; runs in the dedicated `func-tests-live` workflow only.", + "live: hits the real Mergify API. Skipped unless LIVE_TEST_MERGIFY_TOKEN is set; runs in the dedicated `func-tests-live` workflow only.", ] [tool.poe] diff --git a/uv.lock b/uv.lock index 5ac1fff9..936b4484 100644 --- a/uv.lock +++ b/uv.lock @@ -272,58 +272,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -360,7 +308,6 @@ dev = [ { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "pytest-httpserver" }, { name = "responses" }, { name = "respx" }, { name = "ruff" }, @@ -394,7 +341,6 @@ dev = [ { name = "poethepoet", specifier = "==0.45.0" }, { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, - { name = "pytest-httpserver", specifier = "==1.1.3" }, { name = "responses", specifier = "==0.26.0" }, { name = "respx", specifier = "==0.23.1" }, { name = "ruff", specifier = "==0.15.12" }, @@ -714,18 +660,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] -[[package]] -name = "pytest-httpserver" -version = "1.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1049,18 +983,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, ] -[[package]] -name = "werkzeug" -version = "3.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, -] - [[package]] name = "zipp" version = "3.21.0"