Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

653 lines
19 KiB
Python

"""Tests for shared gitlab integration helpers."""
from __future__ import annotations
from typing import Any
import httpx
import pytest
from pydantic import ValidationError
from integrations.gitlab import (
DEFAULT_GITLAB_BASE_URL,
GitlabConfig,
build_gitlab_config,
get_gitlab_commits,
get_gitlab_file,
get_gitlab_mrs,
get_gitlab_pipelines,
gitlab_config_from_env,
post_gitlab_mr_note,
validate_gitlab_config,
validate_gitlab_connection,
)
from integrations.gitlab.verifier import verify_gitlab
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class _FakeResponse:
def __init__(self, payload: Any, status_code: int = 200) -> None:
self._payload = payload
self.status_code = status_code
self.text = ""
def raise_for_status(self) -> None:
if self.status_code >= 400:
raise httpx.HTTPStatusError(
"error",
request=httpx.Request("GET", "https://gitlab.com"),
response=httpx.Response(self.status_code),
)
def json(self) -> Any:
return self._payload
def _make_config(**kwargs: Any) -> GitlabConfig:
return GitlabConfig.model_validate({"auth_token": "test-token", **kwargs})
# ---------------------------------------------------------------------------
# GitlabConfig
# ---------------------------------------------------------------------------
def test_gitlab_config_defaults() -> None:
config = _make_config()
assert config.base_url == DEFAULT_GITLAB_BASE_URL
assert config.timeout_seconds == 15.0
def test_gitlab_config_api_base_url_strips_trailing_slash() -> None:
config = _make_config(base_url="https://gitlab.example.com/api/v4/")
assert config.api_base_url == "https://gitlab.example.com/api/v4"
def test_gitlab_config_auth_headers_include_bearer_token() -> None:
config = _make_config(auth_token="my-secret-token")
assert config.auth_headers == {
"Authorization": "Bearer my-secret-token",
"Accept": "application/json",
}
def test_gitlab_config_normalizes_empty_base_url_to_default() -> None:
config = _make_config(base_url="")
assert config.base_url == DEFAULT_GITLAB_BASE_URL
def test_gitlab_config_normalizes_none_base_url_to_default() -> None:
config = GitlabConfig.model_validate({"auth_token": "tok", "base_url": None})
assert config.base_url == DEFAULT_GITLAB_BASE_URL
def test_gitlab_config_rejects_non_positive_timeout() -> None:
with pytest.raises(ValidationError):
_make_config(timeout_seconds=0)
def test_gitlab_config_rejects_unknown_fields_with_suggestion() -> None:
with pytest.raises(ValidationError, match="auth_toke.*auth_token"):
GitlabConfig.model_validate({"auth_toke": "tok"})
# ---------------------------------------------------------------------------
# build_gitlab_config / gitlab_config_from_env
# ---------------------------------------------------------------------------
def test_build_gitlab_config_accepts_empty_dict() -> None:
config = build_gitlab_config({})
assert config.base_url == DEFAULT_GITLAB_BASE_URL
assert config.auth_token == ""
def test_build_gitlab_config_accepts_none() -> None:
config = build_gitlab_config(None)
assert config.base_url == DEFAULT_GITLAB_BASE_URL
def test_gitlab_config_from_env_returns_none_when_token_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GITLAB_ACCESS_TOKEN", raising=False)
assert gitlab_config_from_env() is None
def test_gitlab_config_from_env_builds_config_from_env(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("GITLAB_ACCESS_TOKEN", "gl-token")
monkeypatch.setenv("GITLAB_BASE_URL", "https://gitlab.example.com/api/v4")
config = gitlab_config_from_env()
assert config is not None
assert config.auth_token == "gl-token"
assert config.base_url == "https://gitlab.example.com/api/v4"
def test_gitlab_config_from_env_uses_default_url_when_not_set(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("GITLAB_ACCESS_TOKEN", "gl-token")
monkeypatch.delenv("GITLAB_BASE_URL", raising=False)
config = gitlab_config_from_env()
assert config is not None
assert config.base_url == DEFAULT_GITLAB_BASE_URL
# ---------------------------------------------------------------------------
# validate_gitlab_config
# ---------------------------------------------------------------------------
def test_validate_gitlab_config_fails_when_token_missing() -> None:
config = build_gitlab_config({})
result = validate_gitlab_config(config)
assert result.ok is False
assert "auth token is required" in result.detail
def test_validate_gitlab_config_returns_ok_on_success(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def _ok(*, config: GitlabConfig) -> dict[str, str]:
del config
return {"username": "gl-user"}
monkeypatch.setattr(
"integrations.gitlab.validate_gitlab_connection",
_ok,
)
result = validate_gitlab_config(_make_config())
assert result.ok is True
assert "@gl-user" in result.detail
def test_validate_gitlab_config_handles_http_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def _raise(*, _config: GitlabConfig) -> None:
response = httpx.Response(401)
response._content = b"Unauthorized"
raise httpx.HTTPStatusError(
"401",
request=httpx.Request("GET", "https://gitlab.com/api/v4/user"),
response=response,
)
monkeypatch.setattr("integrations.gitlab.validate_gitlab_connection", _raise)
result = validate_gitlab_config(_make_config())
assert result.ok is False
assert "validation failed" in result.detail
def test_validate_gitlab_config_handles_generic_exception(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def _boom(*, config: GitlabConfig) -> dict[str, str]:
del config
raise RuntimeError("connection refused")
monkeypatch.setattr(
"integrations.gitlab.validate_gitlab_connection",
_boom,
)
result = validate_gitlab_config(_make_config())
assert result.ok is False
assert "connection refused" in result.detail
def test_verify_gitlab_wraps_validation_result(monkeypatch: pytest.MonkeyPatch) -> None:
def _ok(*, config: GitlabConfig) -> dict[str, str]:
del config
return {"username": "gl-user"}
monkeypatch.setattr("integrations.gitlab.validate_gitlab_connection", _ok)
result = verify_gitlab("local store", {"auth_token": "test-token"})
assert result == {
"service": "gitlab",
"source": "local store",
"status": "passed",
"detail": "GitLab connectivity successful. Authenticated as @gl-user",
}
# ---------------------------------------------------------------------------
# validate_gitlab_connection
# ---------------------------------------------------------------------------
def test_validate_gitlab_connection_returns_user_dict(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse({"id": 1, "username": "gl-user"}),
)
result = validate_gitlab_connection(config=_make_config())
assert result == {"id": 1, "username": "gl-user"}
def test_validate_gitlab_connection_returns_empty_dict_for_non_dict_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(["unexpected", "list"]),
)
result = validate_gitlab_connection(config=_make_config())
assert result == {}
# ---------------------------------------------------------------------------
# get_gitlab_commits
# ---------------------------------------------------------------------------
def test_get_gitlab_commits_returns_list(monkeypatch: pytest.MonkeyPatch) -> None:
commits = [{"id": "abc123", "title": "Fix bug"}]
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(commits),
)
result = get_gitlab_commits(
config=_make_config(),
project_id="my-org/my-repo",
since="2024-01-01T00:00:00Z",
)
assert result == commits
def test_get_gitlab_commits_url_encodes_project_id(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, url: str, **_kw: Any) -> _FakeResponse:
captured["url"] = url
return _FakeResponse([])
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
get_gitlab_commits(
config=_make_config(),
project_id="my-org/my-repo",
since="2024-01-01T00:00:00Z",
)
assert "my-org%2Fmy-repo" in captured["url"]
def test_get_gitlab_commits_returns_empty_list_for_non_list_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse({"error": "not found"}),
)
result = get_gitlab_commits(
config=_make_config(),
project_id="my-org/my-repo",
since="2024-01-01T00:00:00Z",
)
assert result == []
def test_get_gitlab_commits_includes_since_param_when_provided(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, _url: str, **kw: Any) -> _FakeResponse:
captured["params"] = kw.get("params", [])
return _FakeResponse([])
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
get_gitlab_commits(
config=_make_config(),
project_id="proj",
since="2024-06-01T00:00:00Z",
)
param_keys = [k for k, _ in captured["params"]]
assert "since" in param_keys
# ---------------------------------------------------------------------------
# get_gitlab_mrs
# ---------------------------------------------------------------------------
def test_get_gitlab_mrs_returns_list(monkeypatch: pytest.MonkeyPatch) -> None:
mrs = [{"iid": 1, "title": "Add feature"}]
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(mrs),
)
result = get_gitlab_mrs(
config=_make_config(),
project_id="my-org/my-repo",
updated_after="2024-01-01T00:00:00Z",
)
assert result == mrs
def test_get_gitlab_mrs_url_encodes_project_id(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, url: str, **_kw: Any) -> _FakeResponse:
captured["url"] = url
return _FakeResponse([])
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
get_gitlab_mrs(
config=_make_config(),
project_id="my-org/my-repo",
updated_after="2024-01-01T00:00:00Z",
)
assert "my-org%2Fmy-repo" in captured["url"]
def test_get_gitlab_mrs_returns_empty_list_for_non_list_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse({}),
)
result = get_gitlab_mrs(
config=_make_config(),
project_id="proj",
updated_after="2024-01-01T00:00:00Z",
)
assert result == []
# ---------------------------------------------------------------------------
# get_gitlab_pipelines
# ---------------------------------------------------------------------------
def test_get_gitlab_pipelines_returns_list(monkeypatch: pytest.MonkeyPatch) -> None:
pipelines = [{"id": 99, "status": "failed"}]
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(pipelines),
)
result = get_gitlab_pipelines(
config=_make_config(),
project_id="my-org/my-repo",
updated_after="2024-01-01T00:00:00Z",
)
assert result == pipelines
def test_get_gitlab_pipelines_url_encodes_project_id(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, url: str, **_kw: Any) -> _FakeResponse:
captured["url"] = url
return _FakeResponse([])
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
get_gitlab_pipelines(
config=_make_config(),
project_id="my-org/my-repo",
updated_after="2024-01-01T00:00:00Z",
)
assert "my-org%2Fmy-repo" in captured["url"]
def test_get_gitlab_pipelines_returns_empty_list_for_non_list_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse({"unexpected": "dict"}),
)
result = get_gitlab_pipelines(
config=_make_config(),
project_id="proj",
updated_after="2024-01-01T00:00:00Z",
)
assert result == []
# ---------------------------------------------------------------------------
# get_gitlab_file
# ---------------------------------------------------------------------------
def test_get_gitlab_file_returns_dict(monkeypatch: pytest.MonkeyPatch) -> None:
file_data = {"file_name": "README.md", "content": "SGVsbG8="}
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(file_data),
)
result = get_gitlab_file(
config=_make_config(),
project_id="my-org/my-repo",
file_path="README.md",
)
assert result == file_data
def test_get_gitlab_file_url_encodes_project_id_and_path(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, url: str, **_kw: Any) -> _FakeResponse:
captured["url"] = url
return _FakeResponse({})
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
get_gitlab_file(
config=_make_config(),
project_id="my-org/my-repo",
file_path="src/main.py",
)
assert "my-org%2Fmy-repo" in captured["url"]
assert "src%2Fmain.py" in captured["url"]
def test_get_gitlab_file_returns_empty_dict_for_non_dict_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(["not", "a", "dict"]),
)
result = get_gitlab_file(
config=_make_config(),
project_id="proj",
file_path="foo.py",
)
assert result == {}
# ---------------------------------------------------------------------------
# post_gitlab_mr_note
# ---------------------------------------------------------------------------
def test_post_gitlab_mr_note_uses_post_method(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_request(method: str, _url: str, **_kw: Any) -> _FakeResponse:
captured["method"] = method
return _FakeResponse({"id": 1, "body": "RCA Finding"})
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
post_gitlab_mr_note(
config=_make_config(), project_id="my-org/my-repo", mr_iid="42", body="RCA Finding"
)
assert captured["method"] == "POST"
def test_post_gitlab_mr_note_url_encodes_project_id(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, url: str, **_kw: Any) -> _FakeResponse:
captured["url"] = url
return _FakeResponse({"id": 1})
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
post_gitlab_mr_note(config=_make_config(), project_id="my-org/my-repo", mr_iid="7", body="note")
assert "my-org%2Fmy-repo" in captured["url"]
assert "/merge_requests/7/notes" in captured["url"]
def test_post_gitlab_mr_note_sends_body_in_json(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, _url: str, **kw: Any) -> _FakeResponse:
captured["json"] = kw.get("json")
return _FakeResponse({"id": 1})
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
post_gitlab_mr_note(
config=_make_config(), project_id="proj", mr_iid="3", body="root cause is X"
)
assert captured["json"] == {"body": "root cause is X"}
def test_post_gitlab_mr_note_returns_empty_dict_for_non_dict_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse([{"unexpected": "list"}]),
)
result = post_gitlab_mr_note(config=_make_config(), project_id="proj", mr_iid="1", body="note")
assert result == {}
def test_post_gitlab_mr_note_returns_created_note_dict(monkeypatch: pytest.MonkeyPatch) -> None:
note = {"id": 42, "body": "RCA: deployment caused the spike", "author": {"username": "bot"}}
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse(note),
)
result = post_gitlab_mr_note(
config=_make_config(),
project_id="proj",
mr_iid="5",
body="RCA: deployment caused the spike",
)
assert result == note
assert result["id"] == 42
def test_post_gitlab_mr_note_raises_on_403(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse({}, status_code=403),
)
with pytest.raises(httpx.HTTPStatusError):
post_gitlab_mr_note(config=_make_config(), project_id="proj", mr_iid="1", body="note")
def test_post_gitlab_mr_note_raises_on_404(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"integrations.gitlab.httpx.request",
lambda *_, **__: _FakeResponse({}, status_code=404),
)
with pytest.raises(httpx.HTTPStatusError):
post_gitlab_mr_note(config=_make_config(), project_id="proj", mr_iid="999", body="note")
def test_post_gitlab_mr_note_mr_iid_in_url_path(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_request(_method: str, url: str, **_kw: Any) -> _FakeResponse:
captured["url"] = url
return _FakeResponse({"id": 1})
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
post_gitlab_mr_note(config=_make_config(), project_id="proj", mr_iid="123", body="note")
assert "/merge_requests/123/notes" in captured["url"]
def test_post_gitlab_mr_note_sends_multiline_body(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
multiline = "## Root Cause\n\nDeployment at 14:32 UTC introduced a memory leak.\n\n**Fix**: rollback to v1.2.3"
def _fake_request(_method: str, _url: str, **kw: Any) -> _FakeResponse:
captured["json"] = kw.get("json")
return _FakeResponse({"id": 1})
monkeypatch.setattr("integrations.gitlab.httpx.request", _fake_request)
post_gitlab_mr_note(config=_make_config(), project_id="proj", mr_iid="1", body=multiline)
assert captured["json"] == {"body": multiline}