231 lines
9.4 KiB
Python
231 lines
9.4 KiB
Python
"""Version negotiation canary: read-only diagnostics and fail-closed merge gates."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from connectors.gitea import FakeGiteaServer
|
|
from packages.connector_kit import GiteaError
|
|
from tests.test_gitea_connector import connector, context
|
|
from tests.test_gitea_merge_version_compat import _client
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("version", "normalized", "field"),
|
|
[
|
|
("28.0.0", "28.0.0", "do"),
|
|
(" v28.0.0+gitea.1 ", "28.0.0", "do"),
|
|
("v1.25.5+vendor.7", "1.25.5", "Do"),
|
|
("1.26.4", "1.26.4", "do"),
|
|
("1.27.2", "1.27.2", "do"),
|
|
],
|
|
)
|
|
def test_upstream_discovery_normalizes_verified_release(version, normalized, field) -> None:
|
|
client, fixture, _ = _client(version)
|
|
try:
|
|
diagnostics = client.upstream_compatibility()
|
|
assert diagnostics["upstreamVersion"] == version.strip().split("+", 1)[0]
|
|
assert diagnostics["normalizedVersion"] == normalized
|
|
assert diagnostics["compatibility"] == "SUPPORTED"
|
|
assert diagnostics["mergeStrategyField"] == field
|
|
assert diagnostics["source"] == "/api/v1/version"
|
|
assert fixture.recorded_methods == ["GET"]
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"metadata",
|
|
[
|
|
"AKIA" + "A" * 16,
|
|
"glpat-" + "x" * 24,
|
|
"eyJ" + "a" * 15 + "." + "b" * 15 + "." + "c" * 15,
|
|
"fixture-token",
|
|
"a1" * 20,
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("separator", ["+", "-"])
|
|
def test_version_diagnostics_exclude_secret_shaped_metadata(metadata, separator):
|
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0" + separator + metadata))
|
|
try:
|
|
data = instance.capabilities_get(context())["data"]
|
|
diagnostics = data["upstreamGitea"]
|
|
assert metadata not in json.dumps(data)
|
|
assert diagnostics["upstreamVersion"] == "28.0.0"
|
|
assert diagnostics["normalizedVersion"] == "28.0.0"
|
|
assert diagnostics["compatibility"] == ("SUPPORTED" if separator == "+" else "UNSUPPORTED")
|
|
assert "POST" not in fixture.recorded_methods
|
|
finally:
|
|
instance.client.close()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("version", "verdict"),
|
|
[
|
|
("1.24.9", "UNSUPPORTED"),
|
|
("1.28.0", "UNSUPPORTED"),
|
|
("2.0.0", "UNSUPPORTED"),
|
|
("29.0.0", "UNSUPPORTED"),
|
|
("28.0.0-rc.1", "UNSUPPORTED"),
|
|
("", "UNKNOWN"),
|
|
("garbage", "UNKNOWN"),
|
|
("28.0.0+", "UNKNOWN"),
|
|
("28.0.0-", "UNKNOWN"),
|
|
("28.00.0", "UNKNOWN"),
|
|
("28.0.0\nforged", "UNKNOWN"),
|
|
],
|
|
)
|
|
def test_unknown_or_untested_upstream_fails_before_write(version, verdict) -> None:
|
|
client, fixture, repository = _client(version)
|
|
try:
|
|
diagnostics = client.upstream_compatibility()
|
|
assert diagnostics["compatibility"] == verdict
|
|
assert diagnostics["mergeStrategyField"] is None
|
|
with pytest.raises(GiteaError) as raised:
|
|
client.merge_pull_request(repository, 7, method="squash", head_commit_id="e" * 40)
|
|
assert raised.value.code == "GITEA_VERSION_UNSUPPORTED"
|
|
assert "POST" not in fixture.recorded_methods
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
@pytest.mark.parametrize("payload", [{}, {"version": None}, {"version": 28}, []])
|
|
def test_missing_or_non_string_upstream_version_cannot_fall_back_to_connector(payload) -> None:
|
|
instance, fixture = connector()
|
|
try:
|
|
with patch.object(instance.client, "_request", return_value=payload):
|
|
data = instance.capabilities_get(context())["data"]
|
|
assert data["connectorVersion"] == "1.14.0"
|
|
assert data["upstreamGitea"]["compatibility"] == "UNKNOWN"
|
|
assert data["upstreamGitea"]["upstreamVersion"] is None
|
|
assert data["subsystems"]["upstream_gitea"] == "UNVERIFIED"
|
|
assert data["subsystems"]["merge"] == "UNAVAILABLE"
|
|
assert fixture.recorded_methods == []
|
|
finally:
|
|
instance.client.close()
|
|
|
|
|
|
def test_connector_and_server_versions_are_distinct_in_read_only_canary() -> None:
|
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0"))
|
|
try:
|
|
data = instance.capabilities_get(context())["data"]
|
|
assert data["version"] == data["connectorVersion"] == "1.14.0"
|
|
assert data["upstreamGitea"]["upstreamVersion"] == "28.0.0"
|
|
assert data["upstreamGitea"]["compatibility"] == "SUPPORTED"
|
|
assert data["subsystems"]["upstream_gitea"] == "READY"
|
|
assert data["subsystems"]["merge"] == "READY"
|
|
canary = instance.selftest_run(context())["data"]
|
|
assert canary["status"] == "PASS"
|
|
assert any(item["name"] == "upstream-merge-compatibility" for item in canary["checks"])
|
|
assert "POST" not in fixture.recorded_methods
|
|
finally:
|
|
instance.client.close()
|
|
|
|
|
|
def test_diagnostics_cache_expires_and_rechecks_after_dependency_recovery() -> None:
|
|
client, fixture, _ = _client("28.0.0")
|
|
try:
|
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=100):
|
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
|
fixture.gitea_version = "29.0.0"
|
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=120):
|
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=131):
|
|
assert client.upstream_compatibility()["compatibility"] == "UNSUPPORTED"
|
|
fixture.gitea_version = "28.0.0"
|
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=162):
|
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
|
assert fixture.recorded_paths.count("/api/v1/version") == 3
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_merge_rechecks_server_even_with_cached_successful_diagnostics() -> None:
|
|
client, fixture, repository = _client("28.0.0")
|
|
try:
|
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
|
fixture.gitea_version = "29.0.0"
|
|
with pytest.raises(GiteaError) as raised:
|
|
client.merge_pull_request(repository, 7, method="squash", head_commit_id="e" * 40)
|
|
assert raised.value.code == "GITEA_VERSION_UNSUPPORTED"
|
|
assert "POST" not in fixture.recorded_methods
|
|
assert fixture.recorded_paths.count("/api/v1/version") == 2
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_version_outage_invalidates_cached_success_and_recovers() -> None:
|
|
client, fixture, repository = _client("28.0.0")
|
|
try:
|
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
|
with patch.object(
|
|
client,
|
|
"_request",
|
|
side_effect=GiteaError("GITEA_UNAVAILABLE", "upstream request unavailable"),
|
|
):
|
|
with pytest.raises(GiteaError) as raised:
|
|
client.merge_pull_request(repository, 7, method="squash", head_commit_id="e" * 40)
|
|
assert raised.value.code == "GITEA_UNAVAILABLE"
|
|
assert client.upstream_compatibility()["compatibility"] == "UNAVAILABLE"
|
|
assert "POST" not in fixture.recorded_methods
|
|
assert client.upstream_compatibility(force_refresh=True)["compatibility"] == "SUPPORTED"
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_readiness_reports_failed_version_probe_without_disabling_read_service() -> None:
|
|
instance, _ = connector()
|
|
original_request = instance.client._request
|
|
|
|
def request(path, *args, **kwargs):
|
|
if path == "version":
|
|
raise GiteaError("GITEA_UNAVAILABLE", "upstream request unavailable")
|
|
return original_request(path, *args, **kwargs)
|
|
|
|
try:
|
|
with patch.object(instance.client, "_request", side_effect=request):
|
|
data = instance.readiness()
|
|
assert data["status"] == "ready"
|
|
assert data["upstream"] == "unavailable"
|
|
assert data["subsystems"]["upstream_gitea"] == "UNAVAILABLE"
|
|
assert data["subsystems"]["merge"] == "UNAVAILABLE"
|
|
assert data["upstreamGitea"]["errorCode"] == "GITEA_UNAVAILABLE"
|
|
finally:
|
|
instance.client.close()
|
|
|
|
|
|
@pytest.mark.parametrize("mutation", ["wrong-head", "wrong-event", "failed"])
|
|
def test_current_version_still_requires_exact_successful_pr_validation(mutation) -> None:
|
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0"))
|
|
if mutation == "wrong-head":
|
|
fixture._action_run["head_sha"] = "d" * 40
|
|
elif mutation == "wrong-event":
|
|
fixture._action_run["event"] = "workflow_dispatch"
|
|
else:
|
|
fixture._action_run["conclusion"] = "failure"
|
|
try:
|
|
with pytest.raises(GiteaError):
|
|
instance.pull_request_merge(context(), "hub-repository", 7, "e" * 40, "squash")
|
|
assert "/api/v1/repos/acme/hub/pulls/7/merge" not in fixture.recorded_paths
|
|
assert fixture._pulls[0].get("merged") is not True
|
|
finally:
|
|
instance.client.close()
|
|
|
|
|
|
def test_current_release_allows_only_sha_locked_non_forced_merge() -> None:
|
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0"))
|
|
try:
|
|
result = instance.pull_request_merge(context(), "hub-repository", 7, "e" * 40, "squash")[
|
|
"data"
|
|
]
|
|
assert result["validationRunId"] == 42
|
|
body = next(body for body in fixture.recorded_bodies if body.get("do") == "squash")
|
|
assert body["head_commit_id"] == "e" * 40
|
|
assert body["force_merge"] is False
|
|
assert body["merge_when_checks_succeed"] is False
|
|
finally:
|
|
instance.client.close()
|