From 4723e72e3332c52e45c5dfe7c780f0bf4f3e278f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Mon, 13 Apr 2026 09:11:05 -0500 Subject: [PATCH 1/6] Replace ad-hoc SCM RPC with scm-platform RPC --- src/sentry/api/urls.py | 2 +- src/sentry/scm/endpoints/scm_rpc.py | 300 +------ src/sentry/scm/private/helpers.py | 11 +- src/sentry/shared_integrations/client/base.py | 5 +- tests/sentry/scm/endpoints/test_scm_rpc.py | 772 +++--------------- 5 files changed, 152 insertions(+), 938 deletions(-) diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 65037711689f7e..bb76a603c64d78 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -3616,7 +3616,7 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: name="sentry-api-0-rpc-service", ), re_path( - r"^scm-rpc/(?P\w+)/$", + r"^scm-rpc/$", ScmRpcServiceEndpoint.as_view(), name="sentry-api-0-scm-rpc-service", ), diff --git a/src/sentry/scm/endpoints/scm_rpc.py b/src/sentry/scm/endpoints/scm_rpc.py index ae8e2b035332bb..b4d16826f1d46f 100644 --- a/src/sentry/scm/endpoints/scm_rpc.py +++ b/src/sentry/scm/endpoints/scm_rpc.py @@ -1,125 +1,21 @@ -import hashlib -import hmac -import logging -from typing import Any - import sentry_sdk from django.conf import settings -from django.contrib.auth.models import AnonymousUser -from rest_framework.exceptions import ( - AuthenticationFailed, - PermissionDenied, -) +from django.http import HttpResponse, StreamingHttpResponse from rest_framework.request import Request -from rest_framework.response import Response +from scm.rpc.server import RpcServer from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.authentication import AuthenticationSiloLimit, StandardAuthentication from sentry.api.base import Endpoint, internal_cell_silo_endpoint -from sentry.hybridcloud.rpc.service import RpcAuthenticationSetupException -from sentry.scm.errors import ( - SCMCodedError, - SCMError, - SCMProviderException, - SCMProviderNotSupported, - SCMRpcActionCallError, - SCMRpcActionNotFound, - SCMRpcCouldNotDeserializeRequest, - SCMUnhandledException, +from sentry.scm.private.helpers import fetch_repository, fetch_service_provider +from sentry.scm.private.ipc import record_count_metric + +server = RpcServer( + secrets=settings.SCM_RPC_SHARED_SECRET, + fetch_repository=fetch_repository, + fetch_provider=fetch_service_provider, + record_count=record_count_metric, ) -from sentry.scm.private.rpc import dispatch -from sentry.silo.base import SiloMode - -logger = logging.getLogger(__name__) - - -def generate_request_signature(url_path: str, body: bytes) -> str: - """ - Generate a signature for the request body - with the first shared secret. If there are other - shared secrets in the list they are only to be used - for verification during key rotation. - """ - if not settings.SCM_RPC_SHARED_SECRET: - raise RpcAuthenticationSetupException( - "Cannot sign RPC requests without SCM_RPC_SHARED_SECRET" - ) - - signature_input = body - secret = settings.SCM_RPC_SHARED_SECRET[0] - signature = hmac.new(secret.encode("utf-8"), signature_input, hashlib.sha256).hexdigest() - return f"rpc0:{signature}" - - -@AuthenticationSiloLimit(SiloMode.CONTROL, SiloMode.CELL) -class ScmRpcSignatureAuthentication(StandardAuthentication): - """ - Authentication for SCM RPC requests. - Requests are sent with an HMAC signed by a shared private key. - """ - - token_name = b"rpcsignature" - - def accepts_auth(self, auth: list[bytes]) -> bool: - if not auth or len(auth) < 2: - return False - return auth[0].lower() == self.token_name - - def authenticate_token(self, request: Request, token: str) -> tuple[Any, Any]: - signature_validation_error = get_signature_validation_error( - request.path_info, request.body, token - ) - if signature_validation_error: - raise AuthenticationFailed( - { - "errors": [ - _make_error( - exception_type="AuthenticationFailure", - status_code=401, - title="SCM RPC signature validation failed.", - detail=signature_validation_error, - ) - ] - } - ) - - sentry_sdk.get_isolation_scope().set_tag("scm_rpc_auth", True) - - return (AnonymousUser(), token) - - -def get_signature_validation_error(url: str, body: bytes, signature: str) -> str | None: - """ - Compare request data + signature signed by one of the shared secrets. - - Once a key has been able to validate the signature other keys will - not be attempted. We should only have multiple keys during key rotations. - """ - if not settings.SCM_RPC_SHARED_SECRET: - raise RpcAuthenticationSetupException( - "Cannot validate RPC request signatures without SCM_RPC_SHARED_SECRET" - ) - - signature_parts = signature.split(":", 1) - if len(signature_parts) != 2: - return "invalid signature format" - - signature_prefix, signature_data = signature_parts - - if signature_prefix != "rpc0": - return "invalid signature prefix" - - if not body: - return "no body" - - for key in settings.SCM_RPC_SHARED_SECRET: - computed = hmac.new(key.encode(), body, hashlib.sha256).hexdigest() - is_valid = hmac.compare_digest(computed.encode(), signature_data.encode()) - if is_valid: - return None - - return "wrong secret" @internal_cell_silo_endpoint @@ -129,178 +25,18 @@ class ScmRpcServiceEndpoint(Endpoint): Copied from the normal rpc endpoint and modified for use with SCM. """ - publish_status = { - "POST": ApiPublishStatus.EXPERIMENTAL, - } + publish_status = {"POST": ApiPublishStatus.EXPERIMENTAL} owner = ApiOwner.CODING_WORKFLOWS - authentication_classes = (ScmRpcSignatureAuthentication,) + authentication_classes = () permission_classes = () enforce_rate_limit = False - @staticmethod @sentry_sdk.trace - def _is_authorized(request: Request) -> bool: - if request.auth and isinstance( - request.successful_authenticator, ScmRpcSignatureAuthentication - ): - return True - return False + def get(self, request: Request) -> HttpResponse: + resp = server.get(headers=request.headers) + return HttpResponse(content=resp.content, status=resp.status_code, headers=resp.headers) @sentry_sdk.trace - def post(self, request: Request, method_name: str) -> Response: - sentry_sdk.set_tag("rpc.method", method_name) - - if not self._is_authorized(request): - raise PermissionDenied() - - try: - result = dispatch(method_name, request.data) - except SCMRpcActionNotFound as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMRpcActionNotFound", - status_code=404, - title="Not found", - detail=f"Could not find action {e.action_name}", - action_name=e.action_name, - ) - except SCMRpcCouldNotDeserializeRequest as e: - sentry_sdk.capture_exception() - return _make_errors_response( - status_code=400, - errors=[ - _make_error( - exception_type="SCMRpcCouldNotDeserializeRequest", - status_code=400, - title="The request could not be deserialized.", - meta=error, - ) - for error in e.args[0] - ], - ) - except SCMRpcActionCallError as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMRpcActionCallError", - status_code=500, - title="An unexpected error occurred.", - detail=e.message, - action_name=e.action_name, - message=e.message, - ) - except SCMProviderNotSupported as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMProviderNotSupported", - status_code=400, - title="Provider not supported.", - detail=e.message, - ) - except SCMProviderException as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMProviderException", - status_code=503, - title="The service provider raised an error.", - detail=_make_detail(e), - ) - except SCMCodedError as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMCodedError", - code=e.code, - status_code=500, - title="An error occurred.", - detail=_make_detail(e), - ) - except SCMUnhandledException as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMUnhandledException", - status_code=500, - title="An unexpected error occurred.", - detail=_make_detail(e), - ) - except SCMError as e: - sentry_sdk.capture_exception() - return _make_single_error_response( - exception_type="SCMError", - status_code=500, - title="An unexpected error occurred.", - detail=_make_detail(e), - ) - except Exception: - sentry_sdk.capture_exception() - raise - else: - return Response(data={"data": result}) - - -def _make_single_error_response( - *, - exception_type: str, - status_code: int, - title: str, - detail: str, - code: str | None = None, - message: str | None = None, - action_name: str | None = None, -) -> Response: - return _make_errors_response( - status_code=status_code, - errors=[ - _make_error( - status_code=status_code, - title=title, - detail=detail, - exception_type=exception_type, - code=code, - message=message, - action_name=action_name, - ) - ], - ) - - -def _make_errors_response(*, status_code: int, errors: list[dict[str, Any]]) -> Response: - return Response(data={"errors": errors}, status=status_code) - - -def _make_error( - *, - exception_type: str, - status_code: int, - title: str, - detail: str | None = None, - code: str | None = None, - message: str | None = None, - action_name: str | None = None, - meta: dict[str, Any] | None = None, -) -> dict[str, Any]: - error: dict[str, Any] = { - "status": str(status_code), - "title": title, - } - if detail is not None: - error["detail"] = detail - if meta is None: - meta = {} - if exception_type is not None: - meta["exception_type"] = exception_type - if code is not None: - meta["code"] = code - if message is not None: - meta["message"] = message - if action_name is not None: - meta["action_name"] = action_name - if meta: - error["meta"] = meta - return error - - -def _make_detail(e: BaseException) -> str: - details = list(e.args) - while e.__cause__: - e = e.__cause__ - details.extend(e.args) - return ", ".join(str(detail) for detail in details) + def post(self, request: Request) -> StreamingHttpResponse: + resp = server.post(data=request.body, headers=request.headers) + return StreamingHttpResponse(resp.content, status=resp.status_code, headers=resp.headers) diff --git a/src/sentry/scm/private/helpers.py b/src/sentry/scm/private/helpers.py index 152824d79a0690..e13bd64715a62f 100644 --- a/src/sentry/scm/private/helpers.py +++ b/src/sentry/scm/private/helpers.py @@ -1,5 +1,8 @@ from collections.abc import Callable +from scm.providers.github.provider import GitHubProvider +from scm.providers.gitlab.provider import GitLabProvider + from sentry.constants import ObjectStatus from sentry.integrations.base import IntegrationInstallation from sentry.integrations.models.integration import Integration @@ -8,8 +11,6 @@ from sentry.models.repository import Repository as RepositoryModel from sentry.scm.errors import SCMCodedError, SCMError, SCMUnhandledException from sentry.scm.private.ipc import record_count_metric -from sentry.scm.private.providers.github import GitHubProvider -from sentry.scm.private.providers.gitlab import GitLabProvider from sentry.scm.private.rate_limit import RateLimitProvider, RedisRateLimitProvider from sentry.scm.types import ExternalId, Provider, ProviderName, Referrer, Repository, RepositoryId @@ -40,11 +41,13 @@ def map_integration_to_provider( def map_repository_model_to_repository(repository: RepositoryModel) -> Repository: return { + "external_id": repository.external_id, + "id": repository.id, "integration_id": repository.integration_id, + "is_active": repository.status == ObjectStatus.ACTIVE, "name": repository.name, "organization_id": repository.organization_id, - "is_active": repository.status == ObjectStatus.ACTIVE, - "external_id": repository.external_id, + "provider_name": repository.provider.removeprefix("integrations:"), } diff --git a/src/sentry/shared_integrations/client/base.py b/src/sentry/shared_integrations/client/base.py index 488ff398a15f12..558f16c7f0329e 100644 --- a/src/sentry/shared_integrations/client/base.py +++ b/src/sentry/shared_integrations/client/base.py @@ -173,6 +173,7 @@ def _request( timeout: int | None = None, ignore_webhook_errors: bool = False, prepared_request: PreparedRequest | None = None, + stream: bool | None = None, raw_response: Literal[True] = ..., ) -> Response: ... @@ -191,6 +192,7 @@ def _request( timeout: int | None = None, ignore_webhook_errors: bool = False, prepared_request: PreparedRequest | None = None, + stream: bool | None = None, raw_response: bool = ..., ) -> Any: ... @@ -208,6 +210,7 @@ def _request( timeout: int | None = None, ignore_webhook_errors: bool = False, prepared_request: PreparedRequest | None = None, + stream: bool | None = None, raw_response: bool = False, ) -> Any | Response: if allow_redirects is None: @@ -252,7 +255,7 @@ def _request( environment_settings = session.merge_environment_settings( url=finalized_request.url, proxies={}, - stream=None, + stream=stream, verify=self.verify_ssl, cert=None, ) diff --git a/tests/sentry/scm/endpoints/test_scm_rpc.py b/tests/sentry/scm/endpoints/test_scm_rpc.py index fb8e49f01bc7f7..ffba19b20803d3 100644 --- a/tests/sentry/scm/endpoints/test_scm_rpc.py +++ b/tests/sentry/scm/endpoints/test_scm_rpc.py @@ -1,68 +1,71 @@ from __future__ import annotations -import contextlib -from typing import TYPE_CHECKING, Any - -import orjson +import requests +import responses from django.test import override_settings from django.urls import reverse - -if TYPE_CHECKING: - from rest_framework.response import _MonkeyPatchedResponse as Response +from scm.actions import create_branch +from scm.rpc.client import SourceCodeManager +from scm.rpc.helpers import sign_get, sign_post from sentry.constants import ObjectStatus from sentry.models.repository import Repository -from sentry.scm.actions import SourceCodeManager -from sentry.scm.endpoints.scm_rpc import generate_request_signature -from sentry.scm.errors import SCMCodedError, SCMError, SCMProviderException, SCMUnhandledException -from sentry.scm.private.rpc import scm_action_registry from sentry.testutils.cases import APITestCase +from sentry.utils import json -@contextlib.contextmanager -def add_method(method_name: str, method_fn: Any): - # Inject a test-only RPC method for cases that go beyond common arguments validation - assert method_name not in scm_action_registry - scm_action_registry[method_name] = method_fn - try: - yield - finally: - del scm_action_registry[method_name] +def assert_coded_error(response, status_code: int, code: str): + assert response.headers["Content-Type"] == "application/json" + assert response.status_code == status_code, response.content + assert response.json()["errors"][0]["code"] == code -@contextlib.contextmanager -def add_say_hello(): - def say_hello(scm: SourceCodeManager, *, name: str) -> dict[str, str]: - return { - "message": f"Hello, {name}! You are from organization {scm.provider.repository['organization_id']} and repository {scm.provider.repository['name']}." - } +def assert_streaming_coded_error(response, status_code: int, code: str): + assert response.headers["Content-Type"] == "application/json" + assert response.status_code == status_code, response.content + assert json.loads(b"".join(response.streaming_content))["errors"][0]["code"] == code - with add_method("say_hello", say_hello): - yield +class DjangoTestClientSessionAdapter: + """Adapts Django's test client to the scm.rpc.client.Session protocol. -@contextlib.contextmanager -def add_raise_scm_error(error: SCMError): - def raise_scm_error(scm: SourceCodeManager) -> dict[str, str]: - raise error + The RPC client expects a requests-like session. Django's test client returns + Django response objects (including StreamingHttpResponse) which lack methods + like .json() that the upstream scm library relies on. This adapter converts + Django responses to requests.Response objects. + """ - with add_method("raise_scm_error", raise_scm_error): - yield + def __init__(self, client): + self._client = client + def _convert(self, django_response): + resp = requests.Response() + resp.status_code = django_response.status_code + if hasattr(django_response, "streaming_content"): + resp._content = b"".join(django_response.streaming_content) + else: + resp._content = django_response.content + for key, value in django_response.items(): + resp.headers[key] = value + resp.encoding = "utf-8" + return resp -@contextlib.contextmanager -def add_call_missing_provider_method(): - def call_missing_provider_method(scm: SourceCodeManager) -> None: - scm.this_method_does_not_exist() # type: ignore[attr-defined] + def get(self, url, headers=None): + return self._convert(self._client.get(url, headers=headers)) - with add_method("call_missing_provider_method", call_missing_provider_method): - yield + def post(self, url, data=None, headers=None, stream=False): + h = dict(headers) if headers else {} + content_type = h.pop("Content-Type", "application/octet-stream") + return self._convert( + self._client.post(url, data=data, content_type=content_type, headers=h) + ) @override_settings(SCM_RPC_SHARED_SECRET=["a-long-value-that-is-hard-to-guess"]) class TestScmRpc(APITestCase): def setUp(self) -> None: - integration = self.create_integration( + self.url = reverse("sentry-api-0-scm-rpc-service") + self.integration = self.create_integration( organization=self.organization, provider="github", name="Github Test Org", @@ -74,634 +77,103 @@ def setUp(self) -> None: provider="integrations:github", external_id="12345", status=ObjectStatus.ACTIVE, - integration_id=integration.id, + integration_id=self.integration.id, ) - - def call(self, method_name: str, data: Any) -> Response: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": method_name}) - return self.client.post( - path, - data=data, - HTTP_AUTHORIZATION=f"rpcsignature {generate_request_signature(path, orjson.dumps(data))}", + self.rpc_client = SourceCodeManager.make_from_repository_id( + self.organization.id, + self.repo.id, + fetch_base_url=lambda: "", + fetch_signing_secret=lambda: "a-long-value-that-is-hard-to-guess", + session_override=DjangoTestClientSessionAdapter(self.client), ) - def test_simplest_success(self) -> None: - with add_say_hello(): - response = self.call( - "say_hello", - { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, - }, - "meta": {}, - }, - ) - assert response.status_code == 200, response.json() - assert response.json() == { - "data": { - "message": f"Hello, World! You are from organization {self.organization.id} and repository test-org/test-repo." - } - } - - def test_no_authorization_header(self) -> None: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": "say_hello"}) - data = { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, - } + self.default_headers = { + "Authorization": f"rpcsignature {sign_get('a-long-value-that-is-hard-to-guess', self.organization.id, self.repo.id)}", + "X-Organization-Id": str(self.organization.id), + "X-Repository-Id": str(self.repo.id), } - response = self.client.post(path, data=data) - assert response.status_code == 403 - # Response body is built by DRF before we can format it as {"errors": [{"detail": ...}]} - assert response.json() == {"detail": "You do not have permission to perform this action."} - def test_wrong_name_in_authorization_header(self) -> None: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": "say_hello"}) - data = { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, - } - } - response = self.client.post( - path, - data=data, - HTTP_AUTHORIZATION=f"not_rpcsignature {generate_request_signature(path, orjson.dumps(data))}", - ) - assert response.status_code == 403 - assert response.json() == {"detail": "You do not have permission to perform this action."} - - def test_wrong_signature_version_in_authorization_header(self) -> None: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": "say_hello"}) - data = { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, - } - } - response = self.client.post(path, data=data, HTTP_AUTHORIZATION="rpcsignature rpc42:foobar") - assert response.status_code == 401 - assert response.json() == { - "errors": [ - { - "status": "401", - "title": "SCM RPC signature validation failed.", - "detail": "invalid signature prefix", - "meta": {"exception_type": "AuthenticationFailure"}, - } - ] - } - - def test_wrong_signature_in_authorization_header(self) -> None: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": "say_hello"}) - data = { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, - } - } - response = self.client.post( - path, data=data, HTTP_AUTHORIZATION="rpcsignature rpc0:wrong-signature" + @responses.activate + def test_end_to_end(self): + responses.add( + responses.POST, + "https://api.github.com/repos/test-org/test-repo/git/refs", + json={"ref": "refs/heads/test", "object": {"sha": "123"}}, + status=201, ) - assert response.status_code == 401 - assert response.json() == { - "errors": [ - { - "status": "401", - "title": "SCM RPC signature validation failed.", - "detail": "wrong secret", - "meta": {"exception_type": "AuthenticationFailure"}, - } - ] - } - def test_signature_with_more_colons_in_authorization_header(self) -> None: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": "say_hello"}) - data = { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, + result = create_branch(self.rpc_client, "test", sha="123") + assert result["data"]["ref"] == "test" + assert result["data"]["sha"] == "123" + assert len(responses.calls) == 1 + + def test_get(self): + response = self.client.get(self.url, headers=self.default_headers) + assert response.status_code == 200, response.content + + response_json = response.json() + assert response_json == { + "data": { + "id": str(self.repo.id), + "type": "repository", + "attributes": { + "organization_id": self.repo.organization_id, + "name": self.repo.name, + "provider_name": self.repo.provider.removeprefix("integrations:"), + "external_id": self.repo.external_id, + "is_active": self.repo.status == ObjectStatus.ACTIVE, + "integration_id": self.repo.integration_id, + }, } } - response = self.client.post( - path, data=data, HTTP_AUTHORIZATION="rpcsignature rpc0:signature:with:colons" - ) - assert response.status_code == 401 - assert response.json() == { - "errors": [ - { - "status": "401", - "title": "SCM RPC signature validation failed.", - "detail": "wrong secret", - "meta": {"exception_type": "AuthenticationFailure"}, - } - ] - } - def test_signature_without_prefix_in_authorization_header(self) -> None: - path = reverse("sentry-api-0-scm-rpc-service", kwargs={"method_name": "say_hello"}) - data = { - "args": { - "name": "World", - "organization_id": self.organization.id, - "repository_id": self.repo.id, - } - } - response = self.client.post( - path, data=data, HTTP_AUTHORIZATION="rpcsignature signature-without-prefix" + def test_get_invalid_headers(self): + assert_coded_error( + self.client.get(self.url, headers={}), 400, "rpc_malformed_request_headers" ) - assert response.status_code == 401 - assert response.json() == { - "errors": [ - { - "status": "401", - "title": "SCM RPC signature validation failed.", - "detail": "invalid signature format", - "meta": {"exception_type": "AuthenticationFailure"}, - } - ] - } - def test_invalid_endpoint(self) -> None: - response = self.call("not_a_method", {"args": {}}) - assert response.status_code == 404 - assert response.json() == { - "errors": [ - { - "meta": { - "exception_type": "SCMRpcActionNotFound", - "action_name": "not_a_method", - }, - "status": "404", - "title": "Not found", - "detail": "Could not find action not_a_method", - } - ] - } - - def test_no_organization_id(self) -> None: - response = self.call("get_issue_comments_v1", {"args": {"repository_id": 57}, "meta": {}}) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "exception_type": "SCMRpcCouldNotDeserializeRequest", - "loc": ["args", "organization_id"], - "msg": "field required", - "type": "value_error.missing", - }, - } - ] - } - - def test_string_as_organization_id(self) -> None: - response = self.call( - "get_issue_comments_v1", - {"args": {"organization_id": "invalid", "repository_id": 57}, "meta": {}}, + def test_get_missing_auth(self): + assert_coded_error( + self.client.get(self.url, headers={**self.default_headers, "Authorization": ""}), + 401, + "rpc_invalid_grant", ) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "organization_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - } - ] - } - def test_no_repository_id(self) -> None: - response = self.call("get_issue_comments_v1", {"args": {"organization_id": 42}, "meta": {}}) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id"], - "msg": "field required", - "type": "value_error.missing", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - } - ] - } - - def test_string_as_repository_id(self) -> None: - response = self.call( - "get_issue_comments_v1", - {"args": {"organization_id": 42, "repository_id": "invalid"}, "meta": {}}, - ) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, + def test_get_invalid_secret(self): + assert_coded_error( + self.client.get( + self.url, + headers={ + **self.default_headers, + "Authorization": f"rpcsignature {sign_get('s', 1, 2)}", }, - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - ] - } - - def test_dict_with_missing_provider_as_repository_id(self) -> None: - response = self.call( - "get_issue_comments_v1", - { - "args": {"organization_id": 42, "repository_id": {"external_id": "repo1"}}, - "meta": {}, - }, + ), + 401, + "rpc_invalid_grant", ) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id", "provider"], - "msg": "field required", - "type": "value_error.missing", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - ] - } - def test_dict_with_missing_external_id_as_repository_id(self) -> None: - response = self.call( - "get_issue_comments_v1", - {"args": {"organization_id": 42, "repository_id": {"provider": "github"}}, "meta": {}}, + def test_post_invalid_headers(self): + assert_streaming_coded_error( + self.client.post(self.url, headers={}), 400, "rpc_malformed_request_headers" ) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id", "external_id"], - "msg": "field required", - "type": "value_error.missing", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - ] - } - def test_dict_with_extra_attribute_as_repository_id(self) -> None: - response = self.call( - "get_issue_comments_v1", - { - "args": { - "organization_id": 42, - "repository_id": { - "provider": "github", - "external_id": "repo1", - "extra": "value", - }, - }, - "meta": {}, - }, + def test_post_missing_auth(self): + assert_streaming_coded_error( + self.client.post(self.url, headers={**self.default_headers, "Authorization": ""}), + 401, + "rpc_invalid_grant", ) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "loc": ["args", "repository_id", "extra"], - "msg": "extra fields not permitted", - "type": "value_error.extra", - "exception_type": "SCMRpcCouldNotDeserializeRequest", - }, - }, - ] - } - def test_correct_dict_as_repository_id(self) -> None: - with add_say_hello(): - response = self.call( - "say_hello", - { - "args": { - "name": "Vincent", - "organization_id": self.organization.id, - "repository_id": {"provider": "github", "external_id": "12345"}, - }, - "meta": {}, + def test_post_invalid_secret(self): + assert_streaming_coded_error( + self.client.post( + self.url, + data=b"test", + headers={ + **self.default_headers, + "Authorization": f"rpcsignature {sign_post('s', b'test')}", }, - ) - assert response.status_code == 200 - assert response.json() == { - "data": { - "message": f"Hello, Vincent! You are from organization {self.organization.id} and repository test-org/test-repo." - } - } - - def test_missing_method_argument(self) -> None: - with add_say_hello(): - response = self.call( - "say_hello", - { - "args": { - "organization_id": self.organization.id, - "repository_id": self.repo.id, - }, - "meta": {}, - }, - ) - assert response.status_code == 500 - assert response.json() == { - "errors": [ - { - "status": "500", - "title": "An unexpected error occurred.", - "detail": "Error calling method say_hello: add_say_hello..say_hello() missing 1 required keyword-only argument: 'name'", - "meta": { - "exception_type": "SCMRpcActionCallError", - "action_name": "say_hello", - "message": "Error calling method say_hello: add_say_hello..say_hello() missing 1 required keyword-only argument: 'name'", - }, - } - ] - } - - def test_extra_method_argument(self) -> None: - with add_say_hello(): - response = self.call( - "say_hello", - { - "args": { - "organization_id": self.organization.id, - "repository_id": self.repo.id, - "name": "World", - "login": "jacquev6", - }, - "meta": {}, - }, - ) - assert response.status_code == 500 - assert response.json() == { - "errors": [ - { - "status": "500", - "title": "An unexpected error occurred.", - "detail": "Error calling method say_hello: add_say_hello..say_hello() got an unexpected keyword argument 'login'", - "meta": { - "exception_type": "SCMRpcActionCallError", - "action_name": "say_hello", - "message": "Error calling method say_hello: add_say_hello..say_hello() got an unexpected keyword argument 'login'", - }, - } - ] - } - - def test_misspelled_method_argument(self) -> None: - with add_say_hello(): - response = self.call( - "say_hello", - { - "args": { - "organization_id": self.organization.id, - "repository_id": self.repo.id, - "fame": "World", - }, - "meta": {}, - }, - ) - assert response.status_code == 500 - assert response.json() == { - "errors": [ - { - "status": "500", - "title": "An unexpected error occurred.", - "detail": "Error calling method say_hello: add_say_hello..say_hello() got an unexpected keyword argument 'fame'. Did you mean 'name'?", - "meta": { - "exception_type": "SCMRpcActionCallError", - "action_name": "say_hello", - "message": "Error calling method say_hello: add_say_hello..say_hello() got an unexpected keyword argument 'fame'. Did you mean 'name'?", - }, - } - ] - } - - def test_list_as_data(self) -> None: - with add_say_hello(): - response = self.call("say_hello", []) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "exception_type": "SCMRpcCouldNotDeserializeRequest", - "loc": ["args"], - "msg": "field required", - "type": "value_error.missing", - }, - } - ] - } - - def test_empty_dict_as_data(self) -> None: - with add_say_hello(): - response = self.call("say_hello", {}) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "exception_type": "SCMRpcCouldNotDeserializeRequest", - "loc": ["args"], - "msg": "field required", - "type": "value_error.missing", - }, - } - ] - } - - def test_list_as_args(self) -> None: - with add_say_hello(): - response = self.call("say_hello", {"args": []}) - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "exception_type": "SCMRpcCouldNotDeserializeRequest", - "loc": ["args", "organization_id"], - "msg": "field required", - "type": "value_error.missing", - }, - }, - { - "status": "400", - "title": "The request could not be deserialized.", - "meta": { - "exception_type": "SCMRpcCouldNotDeserializeRequest", - "loc": ["args", "repository_id"], - "msg": "field required", - "type": "value_error.missing", - }, - }, - ] - } - - def test_scm_unhandled_exception_in_provider_method(self) -> None: - with add_raise_scm_error(SCMUnhandledException("Blah", 68)): - response = self.call( - "raise_scm_error", - {"args": {"organization_id": self.organization.id, "repository_id": self.repo.id}}, - ) - assert response.status_code == 500 - assert response.json() == { - "errors": [ - { - "status": "500", - "title": "An unexpected error occurred.", - "detail": "Blah, 68", - "meta": {"exception_type": "SCMUnhandledException"}, - } - ] - } - - def test_scm_coded_error_in_provider_method(self) -> None: - with add_raise_scm_error(SCMCodedError("Blah", 68, code="repository_not_found")): - response = self.call( - "raise_scm_error", - {"args": {"organization_id": self.organization.id, "repository_id": self.repo.id}}, - ) - assert response.status_code == 500 - assert response.json() == { - "errors": [ - { - "status": "500", - "title": "An error occurred.", - "detail": "repository_not_found, A repository could not be found., Blah, 68", - "meta": {"exception_type": "SCMCodedError", "code": "repository_not_found"}, - } - ] - } - - def test_scm_error_in_provider_method(self) -> None: - with add_raise_scm_error(SCMError("Blah", 42)): - response = self.call( - "raise_scm_error", - {"args": {"organization_id": self.organization.id, "repository_id": self.repo.id}}, - ) - assert response.status_code == 500 - assert response.json() == { - "errors": [ - { - "status": "500", - "title": "An unexpected error occurred.", - "detail": "Blah, 42", - "meta": {"exception_type": "SCMError"}, - } - ] - } - - def test_attribute_error_in_provider_method_is_treated_as_provider_not_supported(self) -> None: - with add_call_missing_provider_method(): - response = self.call( - "call_missing_provider_method", - {"args": {"organization_id": self.organization.id, "repository_id": self.repo.id}}, - ) - - assert response.status_code == 400 - assert response.json() == { - "errors": [ - { - "status": "400", - "title": "Provider not supported.", - "detail": "call_missing_provider_method is not supported by service-provider GitHubProvider", - "meta": {"exception_type": "SCMProviderNotSupported"}, - } - ] - } - - def test_scm_provider_exception_in_provider_method(self) -> None: - with add_raise_scm_error(SCMProviderException("Blah", 68)): - response = self.call( - "raise_scm_error", - {"args": {"organization_id": self.organization.id, "repository_id": self.repo.id}}, - ) - assert response.status_code == 503 - assert response.json() == { - "errors": [ - { - "status": "503", - "title": "The service provider raised an error.", - "detail": "Blah, 68", - "meta": {"exception_type": "SCMProviderException"}, - } - ] - } + ), + 401, + "rpc_invalid_grant", + ) From 662dd3b84e6f22fd544f45b54fffeeb6fd1ede80 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Mon, 13 Apr 2026 09:33:37 -0500 Subject: [PATCH 2/6] Nuke unused code --- src/sentry/scm/actions.py | 659 - src/sentry/scm/errors.py | 60 +- src/sentry/scm/private/facade.py | 84 - src/sentry/scm/private/helpers.py | 52 +- src/sentry/scm/private/providers/github.py | 1148 -- src/sentry/scm/private/providers/gitlab.py | 742 - src/sentry/scm/private/rate_limit.py | 200 - src/sentry/scm/private/rpc.py | 186 - src/sentry/scm/private/webhooks/github.py | 10 +- src/sentry/scm/types.py | 1035 +- .../test_github_provider_integration.py | 596 - .../integration/test_helpers_integration.py | 144 +- .../test_rate_limit_integration.py | 7 +- .../test_scm_actions_integration.py | 193 - tests/sentry/scm/test_fixtures.py | 1569 -- .../scm/unit/private/test_rate_limit.py | 174 - tests/sentry/scm/unit/test_github_provider.py | 1110 -- tests/sentry/scm/unit/test_gitlab_provider.py | 13362 ---------------- tests/sentry/scm/unit/test_helpers.py | 22 - tests/sentry/scm/unit/test_scm_actions.py | 856 - 20 files changed, 27 insertions(+), 22182 deletions(-) delete mode 100644 src/sentry/scm/actions.py delete mode 100644 src/sentry/scm/private/facade.py delete mode 100644 src/sentry/scm/private/providers/github.py delete mode 100644 src/sentry/scm/private/providers/gitlab.py delete mode 100644 src/sentry/scm/private/rpc.py delete mode 100644 tests/sentry/scm/integration/test_github_provider_integration.py delete mode 100644 tests/sentry/scm/integration/test_scm_actions_integration.py delete mode 100644 tests/sentry/scm/test_fixtures.py delete mode 100644 tests/sentry/scm/unit/private/test_rate_limit.py delete mode 100644 tests/sentry/scm/unit/test_github_provider.py delete mode 100644 tests/sentry/scm/unit/test_gitlab_provider.py delete mode 100644 tests/sentry/scm/unit/test_helpers.py delete mode 100644 tests/sentry/scm/unit/test_scm_actions.py diff --git a/src/sentry/scm/actions.py b/src/sentry/scm/actions.py deleted file mode 100644 index c4d6c0041a79bc..00000000000000 --- a/src/sentry/scm/actions.py +++ /dev/null @@ -1,659 +0,0 @@ -from collections.abc import Callable -from typing import Iterable, Self - -from sentry.integrations.models.integration import Integration -from sentry.integrations.services.integration.model import RpcIntegration -from sentry.models.repository import Repository as RepositoryModel -from sentry.scm.private.facade import Facade -from sentry.scm.private.helpers import ( - fetch_repository, - fetch_service_provider, - initialize_provider, - map_integration_to_provider, - map_repository_model_to_repository, -) -from sentry.scm.private.ipc import record_count_metric -from sentry.scm.private.rate_limit import RateLimitProvider -from sentry.scm.types import ( - ALL_PROTOCOLS, - SHA, - ActionResult, - ArchiveFormat, - ArchiveLink, - BranchName, - BuildConclusion, - BuildStatus, - CheckRun, - CheckRunOutput, - Comment, - Commit, - CompareCommitsProtocol, - CreateBranchProtocol, - CreateCheckRunProtocol, - CreateGitBlobProtocol, - CreateGitCommitProtocol, - CreateGitTreeProtocol, - CreateIssueCommentProtocol, - CreateIssueCommentReactionProtocol, - CreateIssueReactionProtocol, - CreatePullRequestCommentProtocol, - CreatePullRequestCommentReactionProtocol, - CreatePullRequestDraftProtocol, - CreatePullRequestProtocol, - CreatePullRequestReactionProtocol, - CreateReviewCommentFileProtocol, - CreateReviewCommentReplyProtocol, - CreateReviewProtocol, - DeleteIssueCommentProtocol, - DeleteIssueCommentReactionProtocol, - DeleteIssueReactionProtocol, - DeletePullRequestCommentProtocol, - DeletePullRequestCommentReactionProtocol, - DeletePullRequestReactionProtocol, - FileContent, - GetArchiveLinkProtocol, - GetBranchProtocol, - GetCheckRunProtocol, - GetCommitProtocol, - GetCommitsByPathProtocol, - GetCommitsProtocol, - GetFileContentProtocol, - GetGitCommitProtocol, - GetIssueCommentReactionsProtocol, - GetIssueCommentsProtocol, - GetIssueReactionsProtocol, - GetPullRequestCommentReactionsProtocol, - GetPullRequestCommentsProtocol, - GetPullRequestCommitsProtocol, - GetPullRequestDiffProtocol, - GetPullRequestFilesProtocol, - GetPullRequestProtocol, - GetPullRequestReactionsProtocol, - GetPullRequestsProtocol, - GetTreeProtocol, - GitBlob, - GitCommitObject, - GitRef, - GitTree, - InputTreeEntry, - MinimizeCommentProtocol, - PaginatedActionResult, - PaginationParams, - Provider, - PullRequest, - PullRequestCommit, - PullRequestFile, - PullRequestState, - Reaction, - ReactionResult, - Referrer, - Repository, - RepositoryId, - RequestOptions, - RequestReviewProtocol, - ResourceId, - Review, - ReviewComment, - ReviewCommentInput, - ReviewEvent, - ReviewSide, - UpdateBranchProtocol, - UpdateCheckRunProtocol, - UpdatePullRequestProtocol, -) - - -class SourceCodeManager(Facade): - """ - The SourceCodeManager class manages ACLs, rate-limits, environment setup, and a - vendor-agnostic mapping of actions to service-provider commands. The SourceCodeManager - exposes a declarative interface. Developers declare what they want and the concrete - implementation details of what's done are abstracted. - - The SourceCodeManager _will_ throw exceptions. That is its intended operating mode. In your - application code you are expected to catch the base SCMError type. - """ - - @classmethod - def make_from_repository_id( - cls, - organization_id: int, - repository_id: RepositoryId, - *, - referrer: Referrer = "shared", - fetch_repository: Callable[[int, RepositoryId], Repository | None] = fetch_repository, - fetch_service_provider: Callable[ - [int, Repository], Provider | None - ] = fetch_service_provider, - record_count: Callable[[str, int, dict[str, str]], None] = record_count_metric, - ) -> Self: - provider = initialize_provider( - organization_id, - repository_id, - fetch_repository=fetch_repository, - fetch_service_provider=fetch_service_provider, - ) - return cls(provider, referrer=referrer, record_count=record_count) - - @classmethod - def make_from_integration( - cls, - organization_id: int, - repository: RepositoryModel, - integration: Integration | RpcIntegration, - *, - referrer: Referrer = "shared", - rate_limit_provider: RateLimitProvider | None = None, - record_count: Callable[[str, int, dict[str, str]], None] = record_count_metric, - ) -> Self: - provider = initialize_provider( - organization_id, - repository.id, - fetch_repository=lambda _, __: map_repository_model_to_repository(repository), - fetch_service_provider=lambda oid, repo: map_integration_to_provider( - oid, integration, repo, rate_limit_provider=rate_limit_provider - ), - ) - - return cls(provider, referrer=referrer, record_count=record_count) - - -def get_capabilities(scm: SourceCodeManager) -> Iterable[str]: - """Get the names of the protocols implemented by the given SourceCodeManager.""" - for protocol in ALL_PROTOCOLS: - if isinstance(scm, protocol): - yield protocol.__name__ - - -def get_issue_comments( - scm: GetIssueCommentsProtocol, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[Comment]: - """Get comments on an issue.""" - return scm.get_issue_comments(issue_id, pagination, request_options) - - -def create_issue_comment( - scm: CreateIssueCommentProtocol, issue_id: str, body: str -) -> ActionResult[Comment]: - """Create a comment on an issue.""" - return scm.create_issue_comment(issue_id, body) - - -def delete_issue_comment(scm: DeleteIssueCommentProtocol, issue_id: str, comment_id: str) -> None: - """Delete a comment on an issue.""" - return scm.delete_issue_comment(issue_id, comment_id) - - -def get_pull_request( - scm: GetPullRequestProtocol, - pull_request_id: str, - request_options: RequestOptions | None = None, -) -> ActionResult[PullRequest]: - """Get a pull request.""" - return scm.get_pull_request(pull_request_id, request_options) - - -def get_pull_request_comments( - scm: GetPullRequestCommentsProtocol, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[Comment]: - """Get comments on a pull request.""" - return scm.get_pull_request_comments(pull_request_id, pagination, request_options) - - -def create_pull_request_comment( - scm: CreatePullRequestCommentProtocol, pull_request_id: str, body: str -) -> ActionResult[Comment]: - """Create a comment on a pull request.""" - return scm.create_pull_request_comment(pull_request_id, body) - - -def delete_pull_request_comment( - scm: DeletePullRequestCommentProtocol, pull_request_id: str, comment_id: str -) -> None: - """Delete a comment on a pull request.""" - return scm.delete_pull_request_comment(pull_request_id, comment_id) - - -def get_issue_comment_reactions( - scm: GetIssueCommentReactionsProtocol, - issue_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[ReactionResult]: - """Get reactions on an issue comment.""" - return scm.get_issue_comment_reactions(issue_id, comment_id, pagination, request_options) - - -def create_issue_comment_reaction( - scm: CreateIssueCommentReactionProtocol, issue_id: str, comment_id: str, reaction: Reaction -) -> ActionResult[ReactionResult]: - """Create a reaction on an issue comment.""" - return scm.create_issue_comment_reaction(issue_id, comment_id, reaction) - - -def delete_issue_comment_reaction( - scm: DeleteIssueCommentReactionProtocol, issue_id: str, comment_id: str, reaction_id: str -) -> None: - """Delete a reaction on an issue comment.""" - return scm.delete_issue_comment_reaction(issue_id, comment_id, reaction_id) - - -def get_pull_request_comment_reactions( - scm: GetPullRequestCommentReactionsProtocol, - pull_request_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[ReactionResult]: - """Get reactions on a pull request comment.""" - return scm.get_pull_request_comment_reactions( - pull_request_id, comment_id, pagination, request_options - ) - - -def create_pull_request_comment_reaction( - scm: CreatePullRequestCommentReactionProtocol, - pull_request_id: str, - comment_id: str, - reaction: Reaction, -) -> ActionResult[ReactionResult]: - """Create a reaction on a pull request comment.""" - return scm.create_pull_request_comment_reaction(pull_request_id, comment_id, reaction) - - -def delete_pull_request_comment_reaction( - scm: DeletePullRequestCommentReactionProtocol, - pull_request_id: str, - comment_id: str, - reaction_id: str, -) -> None: - """Delete a reaction on a pull request comment.""" - return scm.delete_pull_request_comment_reaction(pull_request_id, comment_id, reaction_id) - - -def get_issue_reactions( - scm: GetIssueReactionsProtocol, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[ReactionResult]: - """Get reactions on an issue.""" - return scm.get_issue_reactions(issue_id, pagination, request_options) - - -def create_issue_reaction( - scm: CreateIssueReactionProtocol, issue_id: str, reaction: Reaction -) -> ActionResult[ReactionResult]: - """Create a reaction on an issue.""" - return scm.create_issue_reaction(issue_id, reaction) - - -def delete_issue_reaction( - scm: DeleteIssueReactionProtocol, issue_id: str, reaction_id: str -) -> None: - """Delete a reaction on an issue.""" - return scm.delete_issue_reaction(issue_id, reaction_id) - - -def get_pull_request_reactions( - scm: GetPullRequestReactionsProtocol, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[ReactionResult]: - """Get reactions on a pull request.""" - return scm.get_pull_request_reactions(pull_request_id, pagination, request_options) - - -def create_pull_request_reaction( - scm: CreatePullRequestReactionProtocol, pull_request_id: str, reaction: Reaction -) -> ActionResult[ReactionResult]: - """Create a reaction on a pull request.""" - return scm.create_pull_request_reaction(pull_request_id, reaction) - - -def delete_pull_request_reaction( - scm: DeletePullRequestReactionProtocol, pull_request_id: str, reaction_id: str -) -> None: - """Delete a reaction on a pull request.""" - return scm.delete_pull_request_reaction(pull_request_id, reaction_id) - - -def get_branch( - scm: GetBranchProtocol, - branch: BranchName, - request_options: RequestOptions | None = None, -) -> ActionResult[GitRef]: - """Get a branch reference.""" - return scm.get_branch(branch, request_options) - - -def create_branch(scm: CreateBranchProtocol, branch: BranchName, sha: SHA) -> ActionResult[GitRef]: - """Create a new branch pointing at the given SHA.""" - return scm.create_branch(branch, sha) - - -def update_branch( - scm: UpdateBranchProtocol, branch: BranchName, sha: SHA, force: bool = False -) -> ActionResult[GitRef]: - """Update a branch to point at a new SHA.""" - return scm.update_branch(branch, sha, force) - - -def create_git_blob( - scm: CreateGitBlobProtocol, content: str, encoding: str -) -> ActionResult[GitBlob]: - """Create a git blob object.""" - return scm.create_git_blob(content, encoding) - - -def get_file_content( - scm: GetFileContentProtocol, - path: str, - ref: str | None = None, - request_options: RequestOptions | None = None, -) -> ActionResult[FileContent]: - return scm.get_file_content(path, ref, request_options) - - -def get_commit( - scm: GetCommitProtocol, - sha: SHA, - request_options: RequestOptions | None = None, -) -> ActionResult[Commit]: - return scm.get_commit(sha, request_options) - - -def get_commits( - scm: GetCommitsProtocol, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[Commit]: - """ - Get a paginated list of commits. - - `ref` is either a branch name, a tag name, or a commit SHA. - Specifying a commit SHA retrieves commits up to the given commit SHA. - - Commits are returned in descending order. Equivalent to `git log ref`. - """ - return scm.get_commits(ref=ref, pagination=pagination, request_options=request_options) - - -def get_commits_by_path( - scm: GetCommitsByPathProtocol, - path: str, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[Commit]: - """ - Get a paginated list of commits for a given filepath. - - `ref` is either a branch name, a tag name, or a commit SHA. - Specifying a commit SHA retrieves commits up to the given commit SHA. - - Commits are returned in descending order. Equivalent to `git log ref`. - """ - return scm.get_commits_by_path( - path=path, ref=ref, pagination=pagination, request_options=request_options - ) - - -def compare_commits( - scm: CompareCommitsProtocol, - start_sha: SHA, - end_sha: SHA, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[Commit]: - return scm.compare_commits(start_sha, end_sha, pagination, request_options) - - -def get_tree( - scm: GetTreeProtocol, - tree_sha: SHA, - recursive: bool = True, - request_options: RequestOptions | None = None, -) -> ActionResult[GitTree]: - return scm.get_tree(tree_sha, recursive=recursive, request_options=request_options) - - -def get_git_commit( - scm: GetGitCommitProtocol, - sha: SHA, - request_options: RequestOptions | None = None, -) -> ActionResult[GitCommitObject]: - return scm.get_git_commit(sha, request_options) - - -def create_git_tree( - scm: CreateGitTreeProtocol, - tree: list[InputTreeEntry], - base_tree: SHA | None = None, -) -> ActionResult[GitTree]: - return scm.create_git_tree(tree, base_tree=base_tree) - - -def create_git_commit( - scm: CreateGitCommitProtocol, message: str, tree_sha: SHA, parent_shas: list[SHA] -) -> ActionResult[GitCommitObject]: - return scm.create_git_commit(message, tree_sha, parent_shas) - - -def get_pull_request_files( - scm: GetPullRequestFilesProtocol, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[PullRequestFile]: - return scm.get_pull_request_files(pull_request_id, pagination, request_options) - - -def get_pull_request_commits( - scm: GetPullRequestCommitsProtocol, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[PullRequestCommit]: - return scm.get_pull_request_commits(pull_request_id, pagination, request_options) - - -def get_pull_request_diff( - scm: GetPullRequestDiffProtocol, - pull_request_id: str, - request_options: RequestOptions | None = None, -) -> ActionResult[str]: - return scm.get_pull_request_diff(pull_request_id, request_options) - - -def get_pull_requests( - scm: GetPullRequestsProtocol, - state: PullRequestState | None = "open", - head: BranchName | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, -) -> PaginatedActionResult[PullRequest]: - return scm.get_pull_requests(state, head, pagination, request_options) - - -def create_pull_request( - scm: CreatePullRequestProtocol, - title: str, - body: str, - head: BranchName, - base: BranchName, -) -> ActionResult[PullRequest]: - return scm.create_pull_request(title, body, head, base) - - -def create_pull_request_draft( - scm: CreatePullRequestDraftProtocol, - title: str, - body: str, - head: BranchName, - base: BranchName, -) -> ActionResult[PullRequest]: - return scm.create_pull_request_draft(title, body, head, base) - - -def update_pull_request( - scm: UpdatePullRequestProtocol, - pull_request_id: str, - title: str | None = None, - body: str | None = None, - state: PullRequestState | None = None, -) -> ActionResult[PullRequest]: - return scm.update_pull_request(pull_request_id, title=title, body=body, state=state) - - -def request_review(scm: RequestReviewProtocol, pull_request_id: str, reviewers: list[str]) -> None: - return scm.request_review(pull_request_id, reviewers) - - -def create_review_comment_file( - scm: CreateReviewCommentFileProtocol, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - side: ReviewSide, -) -> ActionResult[ReviewComment]: - """Leave a review comment on a file.""" - return scm.create_review_comment_file(pull_request_id, commit_id, body, path, side) - - -def create_review_comment_reply( - scm: CreateReviewCommentReplyProtocol, - pull_request_id: str, - body: str, - comment_id: str, -) -> ActionResult[ReviewComment]: - """Leave a review comment in reply to another review comment.""" - return scm.create_review_comment_reply(pull_request_id, body, comment_id) - - -def create_review( - scm: CreateReviewProtocol, - pull_request_id: str, - commit_sha: SHA, - event: ReviewEvent, - comments: list[ReviewCommentInput], - body: str | None = None, -) -> ActionResult[Review]: - return scm.create_review(pull_request_id, commit_sha, event, comments, body=body) - - -def create_check_run( - scm: CreateCheckRunProtocol, - name: str, - head_sha: SHA, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - external_id: str | None = None, - started_at: str | None = None, - completed_at: str | None = None, - output: CheckRunOutput | None = None, -) -> ActionResult[CheckRun]: - return scm.create_check_run( - name, - head_sha, - status=status, - conclusion=conclusion, - external_id=external_id, - started_at=started_at, - completed_at=completed_at, - output=output, - ) - - -def get_check_run( - scm: GetCheckRunProtocol, - check_run_id: ResourceId, - request_options: RequestOptions | None = None, -) -> ActionResult[CheckRun]: - return scm.get_check_run(check_run_id, request_options) - - -def update_check_run( - scm: UpdateCheckRunProtocol, - check_run_id: ResourceId, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - output: CheckRunOutput | None = None, -) -> ActionResult[CheckRun]: - return scm.update_check_run(check_run_id, status=status, conclusion=conclusion, output=output) - - -def minimize_comment(scm: MinimizeCommentProtocol, comment_node_id: str, reason: str) -> None: - return scm.minimize_comment(comment_node_id, reason) - - -def get_archive_link( - scm: GetArchiveLinkProtocol, - ref: str, - archive_format: ArchiveFormat = "tarball", -) -> ActionResult[ArchiveLink]: - """Get a URL to download a repository archive.""" - return scm.get_archive_link(ref, archive_format) - - -__all__ = ( - "SourceCodeManager", - "compare_commits", - "create_branch", - "create_check_run", - "create_git_blob", - "create_git_commit", - "create_git_tree", - "create_issue_comment_reaction", - "create_issue_comment", - "create_issue_reaction", - "create_pull_request_comment_reaction", - "create_pull_request_comment", - "create_pull_request_draft", - "create_pull_request_reaction", - "create_pull_request", - "create_review_comment_file", - "create_review_comment_reply", - "create_review", - "delete_issue_comment_reaction", - "delete_issue_comment", - "delete_issue_reaction", - "delete_pull_request_comment_reaction", - "delete_pull_request_comment", - "delete_pull_request_reaction", - "get_archive_link", - "get_branch", - "get_check_run", - "get_commit", - "get_commits_by_path", - "get_commits", - "get_file_content", - "get_git_commit", - "get_issue_comment_reactions", - "get_issue_comments", - "get_issue_reactions", - "get_pull_request_comment_reactions", - "get_pull_request_comments", - "get_pull_request_commits", - "get_pull_request_diff", - "get_pull_request_files", - "get_pull_request_reactions", - "get_pull_request", - "get_pull_requests", - "get_tree", - "minimize_comment", - "request_review", - "update_branch", - "update_check_run", - "update_pull_request", -) diff --git a/src/sentry/scm/errors.py b/src/sentry/scm/errors.py index 05fd94a15e2e80..769a52d80528b6 100644 --- a/src/sentry/scm/errors.py +++ b/src/sentry/scm/errors.py @@ -1,45 +1,4 @@ -from typing import Literal - -type ErrorCode = Literal[ - "repository_inactive", - "repository_not_found", - "repository_organization_mismatch", - "rate_limit_exceeded", - "integration_not_found", - "unsupported_integration", - "unknown_provider", - "malformed_external_id", -] - -ERROR_CODES: dict[ErrorCode, str] = { - "repository_inactive": "A repository was found but it is inactive.", - "repository_not_found": "A repository could not be found.", - "repository_organization_mismatch": "A repository was found but it did not belong to your organization.", - "rate_limit_exceeded": "Exhausted allocated service-provider quota.", - "integration_not_found": "An unsupported integration provider was found.", - "unsupported_integration": "An unsupported integration provider was found.", - "unknown_provider": "Could not resolve source code management provider.", - "malformed_external_id": "The repository's external ID was malformed.", -} - - -class SCMError(Exception): - pass - - -class SCMCodedError(SCMError): - def __init__(self, *args, code: ErrorCode, **kwargs) -> None: - self.code = code - self.message = ERROR_CODES[code] - super().__init__(self.code, self.message, *args, **kwargs) - - -class SCMUnhandledException(SCMError): - pass - - -class SCMProviderException(SCMError): - pass +from scm.errors import SCMError class SCMProviderEventNotSupported(SCMError): @@ -52,20 +11,3 @@ class SCMProviderNotSupported(SCMError): def __init__(self, message: str) -> None: self.message = message super().__init__(message) - - -class SCMRpcActionCallError(SCMError): - def __init__(self, action_name: str, error_message: str) -> None: - self.action_name = action_name - self.message = f"Error calling method {action_name}: {error_message}" - super().__init__(self.message) - - -class SCMRpcActionNotFound(SCMError): - def __init__(self, action_name: str) -> None: - self.action_name = action_name - super().__init__(action_name) - - -class SCMRpcCouldNotDeserializeRequest(SCMError): - pass diff --git a/src/sentry/scm/private/facade.py b/src/sentry/scm/private/facade.py deleted file mode 100644 index 5d15cc7904ac9a..00000000000000 --- a/src/sentry/scm/private/facade.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -from collections.abc import Hashable -from functools import lru_cache -from typing import Any, Callable, cast - -from sentry.scm.private.helpers import exec_provider_fn -from sentry.scm.private.ipc import record_count_metric -from sentry.scm.types import ALL_PROTOCOLS, Provider, Referrer - - -def _delegating_method(name: str) -> Callable[..., Any]: - """Return a method that forwards calls to self.provider..""" - - def method(self: Facade, *args: Any, **kwargs: Any) -> Any: - return exec_provider_fn( - self.provider, - referrer=self.referrer, - provider_fn=lambda: getattr(self.provider, name)(*args, **kwargs), - record_count=self.record_count, - ) - - method.__name__ = name - return method - - -def _protocol_attrs(proto: object) -> tuple[str, ...]: - """Return the runtime protocol attribute names used for capability detection.""" - return cast(tuple[str, ...], getattr(proto, "__protocol_attrs__", ())) - - -@lru_cache(maxsize=32) -def _facade_type_for_provider_class( - cls: type[Facade], provider_cls: type[Provider] -) -> type[Facade]: - """Build (and cache) one facade subclass per implementation class.""" - methods: dict[str, Any] = {} - for proto in ALL_PROTOCOLS: - protocol_attrs = _protocol_attrs(proto) - if all(hasattr(provider_cls, attr) for attr in protocol_attrs): - for attr in protocol_attrs: - if attr not in methods: - methods[attr] = _delegating_method(attr) - return type(f"FacadeFor{provider_cls.__name__}", (cls,), methods) - - -class Facade: - # `Facade` itself declares no capability methods, so MyPy rejects direct - # calls like `facade.create_issue_comment()` and forces `isinstance` guards. - # - # At construction time __new__ builds a private subclass that has exactly - # the methods supported by `impl` as real class-body attributes. Python - # 3.12+ runtime_checkable isinstance() checks look at the class body, not - # __getattr__, so this is what makes `isinstance(facade, protocol)` work. - # - # After the isinstance guard MyPy narrows `facade` to `Facade & Protocol` - # and statically validates method calls. - - provider: Provider - referrer: Referrer - record_count: Callable[[str, int, dict[str, str]], None] - - def __new__( - cls, - provider: Provider, - *, - referrer: Referrer = "shared", - record_count: Callable[[str, int, dict[str, str]], None] = record_count_metric, - ) -> Facade: - facade_cls = _facade_type_for_provider_class( - cast(Hashable, cls), cast(Hashable, type(provider)) - ) - return object.__new__(facade_cls) - - def __init__( - self, - provider: Provider, - *, - referrer: Referrer = "shared", - record_count: Callable[[str, int, dict[str, str]], None] = record_count_metric, - ) -> None: - self.provider = provider - self.referrer = referrer - self.record_count = record_count diff --git a/src/sentry/scm/private/helpers.py b/src/sentry/scm/private/helpers.py index e13bd64715a62f..01280aaf005b62 100644 --- a/src/sentry/scm/private/helpers.py +++ b/src/sentry/scm/private/helpers.py @@ -9,10 +9,9 @@ from sentry.integrations.services.integration.model import RpcIntegration from sentry.integrations.services.integration.service import integration_service from sentry.models.repository import Repository as RepositoryModel -from sentry.scm.errors import SCMCodedError, SCMError, SCMUnhandledException -from sentry.scm.private.ipc import record_count_metric +from sentry.scm.errors import SCMCodedError from sentry.scm.private.rate_limit import RateLimitProvider, RedisRateLimitProvider -from sentry.scm.types import ExternalId, Provider, ProviderName, Referrer, Repository, RepositoryId +from sentry.scm.types import ExternalId, Provider, ProviderName, Repository def map_integration_to_provider( @@ -81,50 +80,3 @@ def fetch_repository( return None return map_repository_model_to_repository(repo) - - -def initialize_provider( - organization_id: int, - repository_id: RepositoryId, - *, - fetch_repository: Callable[[int, RepositoryId], Repository | None] = fetch_repository, - fetch_service_provider: Callable[[int, Repository], Provider | None] = fetch_service_provider, -) -> Provider: - repository = fetch_repository(organization_id, repository_id) - if not repository: - raise SCMCodedError(organization_id, repository_id, code="repository_not_found") - if not repository["is_active"]: - raise SCMCodedError(repository, code="repository_inactive") - if repository["organization_id"] != organization_id: - raise SCMCodedError(repository, code="repository_organization_mismatch") - - provider = fetch_service_provider(organization_id, repository) - if provider is None: - raise SCMCodedError(code="integration_not_found") - - return provider - - -def exec_provider_fn[P: Provider, T]( - provider: P, - *, - referrer: Referrer = "shared", - provider_fn: Callable[[], T], - record_count: Callable[[str, int, dict[str, str]], None] = record_count_metric, -) -> T: - if provider.is_rate_limited(referrer): - raise SCMCodedError(provider, referrer, code="rate_limit_exceeded") - - provider_name = provider.__class__.__name__ - - try: - result = provider_fn() - record_count("sentry.scm.actions.success_by_provider", 1, {"provider": provider_name}) - record_count("sentry.scm.actions.success_by_referrer", 1, {"referrer": referrer}) - return result - except SCMError: - raise - except Exception as e: - record_count("sentry.scm.actions.failed_by_provider", 1, {"provider": provider_name}) - record_count("sentry.scm.actions.failed_by_referrer", 1, {"referrer": referrer}) - raise SCMUnhandledException from e diff --git a/src/sentry/scm/private/providers/github.py b/src/sentry/scm/private/providers/github.py deleted file mode 100644 index 9d0f12372ce26e..00000000000000 --- a/src/sentry/scm/private/providers/github.py +++ /dev/null @@ -1,1148 +0,0 @@ -import time -from collections.abc import Callable -from datetime import datetime -from email.utils import format_datetime, parsedate_to_datetime -from typing import Any, cast - -import requests - -from sentry.integrations.github.client import GitHubApiClient -from sentry.scm.errors import SCMProviderException -from sentry.scm.private.rate_limit import ( - DynamicRateLimiter, - RateLimitProvider, -) -from sentry.scm.types import ( - SHA, - ActionResult, - ArchiveFormat, - ArchiveLink, - Author, - BranchName, - BuildConclusion, - BuildStatus, - CheckRun, - CheckRunOutput, - Comment, - Commit, - CommitAuthor, - CommitFile, - FileContent, - FileStatus, - GitBlob, - GitCommitObject, - GitCommitTree, - GitRef, - GitTree, - InputTreeEntry, - PaginatedActionResult, - PaginatedResponseMeta, - PaginationParams, - PullRequest, - PullRequestBranch, - PullRequestCommit, - PullRequestFile, - PullRequestState, - Reaction, - ReactionResult, - Referrer, - Repository, - RequestOptions, - ResourceId, - ResponseMeta, - Review, - ReviewComment, - ReviewCommentInput, - ReviewEvent, - ReviewSide, - TreeEntry, -) -from sentry.shared_integrations.exceptions import ApiError - -# GitHub's Checks API status values map to generic BuildStatus. -# "requested", "waiting", and "pending" are GitHub Actions-internal states that -# cannot be set via the API; we treat them as "pending" when reading. -GITHUB_STATUS_MAP: dict[str, BuildStatus] = { - "queued": "pending", - "requested": "pending", - "waiting": "pending", - "pending": "pending", - "in_progress": "running", - "completed": "completed", -} - -# GitHub's conclusion values map 1-to-1 except "stale" (GitHub-internal, set -# automatically after 14 days) which we surface as "unknown". -GITHUB_CONCLUSION_MAP: dict[str, BuildConclusion] = { - "success": "success", - "failure": "failure", - "neutral": "neutral", - "cancelled": "cancelled", - "skipped": "skipped", - "timed_out": "timed_out", - "action_required": "action_required", - "stale": "unknown", -} - -# Reverse maps for writing to GitHub's Checks API. -# "pending" maps to "queued" (the only writable in-queue state). -# "unknown" has no GitHub equivalent and is omitted; callers should not write it. -GITHUB_STATUS_WRITE_MAP: dict[BuildStatus, str] = { - "pending": "queued", - "running": "in_progress", - "completed": "completed", -} - -GITHUB_CONCLUSION_WRITE_MAP: dict[BuildConclusion, str] = { - "success": "success", - "failure": "failure", - "neutral": "neutral", - "cancelled": "cancelled", - "skipped": "skipped", - "timed_out": "timed_out", - "action_required": "action_required", - "unknown": "neutral", -} - -GITHUB_ARCHIVE_FORMAT_MAP: dict[ArchiveFormat, str] = { - "tarball": "tarball", - "zip": "zipball", -} - -GITHUB_REVIEW_EVENT_MAP: dict[ReviewEvent, str] = { - "approve": "APPROVE", - "change_request": "REQUEST_CHANGES", - "comment": "COMMENT", -} - - -MINIMIZE_COMMENT_MUTATION = """ -mutation MinimizeComment($commentId: ID!, $reason: ReportedContentClassifiers!) { - minimizeComment(input: {subjectId: $commentId, classifier: $reason}) { - minimizedComment { isMinimized } - } -} -""" - - -# Mapping of referrer, percentage pairs. For a given referrer X% of quota is reserved for that -# identifier. Excess use of the allocated quota does not result in a rate-limit error. Once -# reserved quota is exhausted the referrer will fall back to the shared quota pool. -# -# WARN: "shared" is a reserved referrer name and may not be used. -REFERRER_ALLOCATION: dict[Referrer, float] = {"emerge": 0.05} -assert "shared" not in REFERRER_ALLOCATION - -GITHUB_RATE_LIMIT_WINDOW = 3600 -GITHUB_RATE_LIMIT_CAPACITY = "x-ratelimit-limit" -GITHUB_RATE_LIMIT_USED = "x-ratelimit-used" -GITHUB_RATE_LIMIT_RESET = "x-ratelimit-reset" -GITHUB_RATE_LIMIT_REMAINING = "x-ratelimit-remaining" -GITHUB_RATE_LIMIT_RETRY_AFTER = "retry-after" - - -def _extract_response_meta(response: requests.Response) -> ResponseMeta: - meta: ResponseMeta = {} - if etag := response.headers.get("ETag"): - meta["etag"] = etag - if last_modified := response.headers.get("Last-Modified"): - meta["last_modified"] = parsedate_to_datetime(last_modified) - return meta - - -class GitHubProviderApiClient: - def __init__( - self, - client: GitHubApiClient, - organization_id: int, - rate_limit_provider: RateLimitProvider, - get_time_in_seconds: Callable[[], int] = lambda: int(time.time()), - ) -> None: - self.client = client - self.rate_limiter = DynamicRateLimiter( - get_time_in_seconds=get_time_in_seconds, - organization_id=organization_id, - provider="github", - rate_limit_provider=rate_limit_provider, - rate_limit_window_seconds=GITHUB_RATE_LIMIT_WINDOW, - referrer_allocation=REFERRER_ALLOCATION, - recorded_capacity=None, - ) - - def is_rate_limited(self, referrer: Referrer) -> bool: - """Return true if access to the resource has been blocked.""" - # If the referrer has allocated quota and that quota has not been exhausted we eagerly - # exit by returning false. Otherwise we consume from the shared quota pool. - if ( - referrer in self.rate_limiter.referrer_allocation - and not self.rate_limiter.is_rate_limited(referrer) - ): - return False - else: - return self.rate_limiter.is_rate_limited("shared") - - def request( - self, - method: str, - path: str, - data: dict[str, Any] | None = None, - params: dict[str, str] | None = None, - headers: dict[str, str] | None = None, - allow_redirects: bool | None = None, - ) -> requests.Response: - try: - response = self.client._request( - method=method, - path=path, - headers=headers, - data=data, - params=params, - raw_response=True, - allow_redirects=allow_redirects, - ) - - # If GitHub returned rate-limit information we update our internal representation - # to match. - if ( - GITHUB_RATE_LIMIT_CAPACITY in response.headers - and GITHUB_RATE_LIMIT_USED in response.headers - and GITHUB_RATE_LIMIT_RESET in response.headers - ): - self.rate_limiter.update_rate_limit_meta( - capacity=int(response.headers[GITHUB_RATE_LIMIT_CAPACITY]), - consumed=int(response.headers[GITHUB_RATE_LIMIT_USED]), - next_window_start=int(response.headers[GITHUB_RATE_LIMIT_RESET]), - ) - - # TODO: GitHub tells us when we've hit a rate-limit. We could update our system to - # match. However, I feel there's some subtlety here. Is retry-after API wide - # or just for the requested resource? GitHub tells us its reset but our clocks - # do not agree. How do we ensure we're not blocking? Probably time bucket - # comparisons like we do elsewhere. - # - # # From GitHub: - # # > Continuing to make requests while you are rate limited may result in the - # # > banning of your integration. - # if response.status_code in (403, 429): - # # A secondary rate-limit was breached. Back off for "retry-after" seconds. - # if GITHUB_RATE_LIMIT_RETRY_AFTER in response.headers: - # retry_after = int(response.headers[GITHUB_RATE_LIMIT_RETRY_AFTER]) - - # # A primary rate-limit was breached. No requests until the next window. - # elif ( - # GITHUB_RATE_LIMIT_RESET in response.headers - # and GITHUB_RATE_LIMIT_REMAINING in response.headers - # and response.headers[GITHUB_RATE_LIMIT_REMAINING] == "0" - # ): - # next_window_start = int(response.headers[GITHUB_RATE_LIMIT_RESET]) - - response.raise_for_status() - return response - except (requests.RequestException, ApiError) as e: - raise SCMProviderException(str(e)) from e - - def get( - self, - path: str, - params: dict[str, Any] | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - extra_headers: dict[str, str] | None = None, - allow_redirects: bool | None = None, - ) -> requests.Response: - headers = {"Accept": "application/vnd.github+json"} - - if request_options: - if_none_match = request_options.get("if_none_match") - if if_none_match is not None: - headers["If-None-Match"] = if_none_match - - if_modified_since = request_options.get("if_modified_since") - if if_modified_since is not None: - headers["If-Modified-Since"] = format_datetime(if_modified_since) - - if extra_headers: - headers.update(extra_headers) - - params = params or {} - if pagination: - params["per_page"] = str(pagination["per_page"]) - params["page"] = str(pagination["cursor"]) - - return self.request( - "GET", - path=path, - params=params, - headers=headers, - allow_redirects=allow_redirects, - ) - - def post( - self, - path: str, - data: dict[str, Any], - headers: dict[str, str] | None = None, - ) -> requests.Response: - return self.request("POST", path=path, data=data, headers=headers) - - def patch( - self, - path: str, - data: dict[str, Any], - headers: dict[str, str] | None = None, - ) -> requests.Response: - return self.request("PATCH", path=path, data=data, headers=headers) - - def delete(self, path: str) -> requests.Response: - return self.request("DELETE", path=path) - - def graphql( - self, - query: str, - variables: dict[str, Any], - ) -> dict[str, Any]: - payload: dict[str, Any] = {"query": query} - if variables: - payload["variables"] = variables - - response = self.post("/graphql", data=payload, headers={}) - response_data = response.json() - - if not isinstance(response_data, dict) or ( - "data" not in response_data and "errors" not in response_data - ): - raise SCMProviderException("GraphQL response is not in expected format") - - errors = response_data.get("errors", []) - if errors and not response_data.get("data"): - err_message = "\n".join(e.get("message", "") for e in errors) - raise SCMProviderException(err_message) - - return response_data.get("data", {}) - - -class GitHubProvider: - def __init__( - self, - client: GitHubApiClient, - organization_id: int, - repository: Repository, - rate_limit_provider: RateLimitProvider, - get_time_in_seconds: Callable[[], int] = lambda: int(time.time()), - ) -> None: - self.client = GitHubProviderApiClient( - client, - organization_id=organization_id, - rate_limit_provider=rate_limit_provider, - get_time_in_seconds=get_time_in_seconds, - ) - self.organization_id = organization_id - self.repository = repository - - def is_rate_limited(self, referrer: Referrer) -> bool: - return self.client.is_rate_limited(referrer) - - def get_issue_comments( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: - response = self.client.get( - f"/repos/{self.repository['name']}/issues/{issue_id}/comments", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action(pagination, response, lambda r: [map_comment(c) for c in r]) - - def create_issue_comment(self, issue_id: str, body: str) -> ActionResult[Comment]: - response = self.client.post( - f"/repos/{self.repository['name']}/issues/{issue_id}/comments", - data={"body": body}, - ) - return map_action(response, map_comment) - - def delete_issue_comment(self, issue_id: str, comment_id: str) -> None: - self.client.delete(f"/repos/{self.repository['name']}/issues/comments/{comment_id}") - - def get_pull_request( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[PullRequest]: - response = self.client.get( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}", - request_options=request_options, - ) - return map_action(response, map_pull_request) - - def get_pull_request_comments( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: - response = self.client.get( - f"/repos/{self.repository['name']}/issues/{pull_request_id}/comments", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action(pagination, response, lambda r: [map_comment(c) for c in r]) - - def create_pull_request_comment(self, pull_request_id: str, body: str) -> ActionResult[Comment]: - response = self.client.post( - f"/repos/{self.repository['name']}/issues/{pull_request_id}/comments", - data={"body": body}, - ) - return map_action(response, map_comment) - - def delete_pull_request_comment(self, pull_request_id: str, comment_id: str) -> None: - self.client.delete(f"/repos/{self.repository['name']}/issues/comments/{comment_id}") - - def get_issue_comment_reactions( - self, - issue_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - response = self.client.get( - f"/repos/{self.repository['name']}/issues/comments/{comment_id}/reactions", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action(pagination, response, lambda r: [map_reaction(c) for c in r]) - - def create_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - response = self.client.post( - f"/repos/{self.repository['name']}/issues/comments/{comment_id}/reactions", - data={"content": reaction}, - ) - return map_action(response, map_reaction) - - def delete_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction_id: str - ) -> None: - self.client.delete( - f"/repos/{self.repository['name']}/issues/comments/{comment_id}/reactions/{reaction_id}" - ) - - def get_pull_request_comment_reactions( - self, - pull_request_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - return self.get_issue_comment_reactions( - pull_request_id, comment_id, pagination, request_options - ) - - def create_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - return self.create_issue_comment_reaction(pull_request_id, comment_id, reaction) - - def delete_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction_id: str - ) -> None: - return self.delete_issue_comment_reaction(pull_request_id, comment_id, reaction_id) - - def get_issue_reactions( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - response = self.client.get( - f"/repos/{self.repository['name']}/issues/{issue_id}/reactions", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action(pagination, response, lambda r: [map_reaction(c) for c in r]) - - def create_issue_reaction( - self, issue_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - response = self.client.post( - f"/repos/{self.repository['name']}/issues/{issue_id}/reactions", - data={"content": reaction}, - ) - return map_action(response, map_reaction) - - def delete_issue_reaction(self, issue_id: str, reaction_id: str) -> None: - self.client.delete( - f"/repos/{self.repository['name']}/issues/{issue_id}/reactions/{reaction_id}" - ) - - def get_pull_request_reactions( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - return self.get_issue_reactions(pull_request_id, pagination, request_options) - - def create_pull_request_reaction( - self, pull_request_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - return self.create_issue_reaction(pull_request_id, reaction) - - def delete_pull_request_reaction(self, pull_request_id: str, reaction_id: str) -> None: - return self.delete_issue_reaction(pull_request_id, reaction_id) - - def get_branch( - self, - branch: BranchName, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitRef]: - response = self.client.get( - f"/repos/{self.repository['name']}/branches/{branch}", - request_options=request_options, - ) - return map_action(response, lambda r: GitRef(ref=r["name"], sha=r["commit"]["sha"])) - - def create_branch(self, branch: BranchName, sha: SHA) -> ActionResult[GitRef]: - ref = f"refs/heads/{branch}" - response = self.client.post( - f"/repos/{self.repository['name']}/git/refs", - data={"ref": ref, "sha": sha}, - ) - return map_action( - response, - lambda r: GitRef(ref=r["ref"].removeprefix("refs/heads/"), sha=r["object"]["sha"]), - ) - - def update_branch( - self, branch: BranchName, sha: SHA, force: bool = False - ) -> ActionResult[GitRef]: - response = self.client.patch( - f"/repos/{self.repository['name']}/git/refs/heads/{branch}", - data={"sha": sha, "force": force}, - ) - return map_action( - response, - lambda r: GitRef(ref=r["ref"].removeprefix("refs/heads/"), sha=r["object"]["sha"]), - ) - - def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: - response = self.client.post( - f"/repos/{self.repository['name']}/git/blobs", - data={"content": content, "encoding": encoding}, - ) - return map_action(response, map_git_blob) - - def get_file_content( - self, - path: str, - ref: str | None = None, - request_options: RequestOptions | None = None, - ) -> ActionResult[FileContent]: - params: dict[str, str] = {} - if ref: - params["ref"] = ref - response = self.client.get( - f"/repos/{self.repository['name']}/contents/{path}", - params=params, - request_options=request_options, - ) - return map_action(response, map_file_content) - - def get_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[Commit]: - response = self.client.get( - f"/repos/{self.repository['name']}/commits/{sha}", - request_options=request_options, - ) - return map_action(response, map_commit) - - def get_commits( - self, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - params: dict[str, str] = {} - if ref: - params["sha"] = ref - response = self.client.get( - f"/repos/{self.repository['name']}/commits", - params=params, - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action(pagination, response, lambda r: [map_commit(c) for c in r]) - - def get_commits_by_path( - self, - path: str, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - params: dict[str, str] = {"path": path} - if ref: - params["sha"] = ref - response = self.client.get( - f"/repos/{self.repository['name']}/commits", - params=params, - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action(pagination, response, lambda r: [map_commit(c) for c in r]) - - def compare_commits( - self, - start_sha: SHA, - end_sha: SHA, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - response = self.client.get( - f"/repos/{self.repository['name']}/compare/{start_sha}...{end_sha}", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action( - pagination, response, lambda r: [map_commit(c) for c in r["commits"]] - ) - - def get_tree( - self, - tree_sha: SHA, - recursive: bool = True, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitTree]: - params: dict[str, Any] = {} - if recursive: - params["recursive"] = 1 - response = self.client.get( - f"/repos/{self.repository['name']}/git/trees/{tree_sha}", - params=params, - request_options=request_options, - ) - return map_action(response, map_git_tree) - - def get_git_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitCommitObject]: - response = self.client.get( - f"/repos/{self.repository['name']}/git/commits/{sha}", - request_options=request_options, - ) - return map_action(response, map_git_commit_object) - - def create_git_tree( - self, - tree: list[InputTreeEntry], - base_tree: SHA | None = None, - ) -> ActionResult[GitTree]: - data: dict[str, Any] = {"tree": tree} - if base_tree is not None: - data["base_tree"] = base_tree - response = self.client.post( - f"/repos/{self.repository['name']}/git/trees", - data=data, - ) - return map_action(response, map_git_tree) - - def create_git_commit( - self, - message: str, - tree_sha: SHA, - parent_shas: list[SHA], - ) -> ActionResult[GitCommitObject]: - response = self.client.post( - f"/repos/{self.repository['name']}/git/commits", - data={ - "message": message, - "tree": tree_sha, - "parents": parent_shas, - }, - ) - return map_action(response, map_git_commit_object) - - def get_pull_request_files( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestFile]: - response = self.client.get( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}/files", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action( - pagination, response, lambda r: [map_pull_request_file(f) for f in r] - ) - - def get_pull_request_commits( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestCommit]: - response = self.client.get( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}/commits", - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action( - pagination, response, lambda r: [map_pull_request_commit(c) for c in r] - ) - - def get_pull_request_diff( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[str]: - response = self.client.get( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}", - request_options=request_options, - extra_headers={"Accept": "application/vnd.github.v3.diff"}, - ) - return { - "data": response.text, - "type": "github", - "raw": {"data": response.text, "headers": dict(response.headers)}, - "meta": _extract_response_meta(response), - } - - def get_pull_requests( - self, - state: PullRequestState | None = "open", - head: BranchName | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequest]: - params: dict[str, Any] = {"state": state if state is not None else "all"} - if head: - params["head"] = head - - response = self.client.get( - f"/repos/{self.repository['name']}/pulls", - params=params, - pagination=pagination, - request_options=request_options, - ) - return map_paginated_action( - pagination, response, lambda r: [map_pull_request(pr) for pr in r] - ) - - def create_pull_request( - self, - title: str, - body: str, - head: str, - base: str, - ) -> ActionResult[PullRequest]: - data: dict[str, Any] = { - "title": title, - "body": body, - "head": head, - "base": base, - } - response = self.client.post(f"/repos/{self.repository['name']}/pulls", data=data) - return map_action(response, map_pull_request) - - def create_pull_request_draft( - self, - title: str, - body: str, - head: str, - base: str, - ) -> ActionResult[PullRequest]: - response = self.client.post( - f"/repos/{self.repository['name']}/pulls", - data={"title": title, "body": body, "head": head, "base": base, "draft": True}, - ) - return map_action(response, map_pull_request) - - def update_pull_request( - self, - pull_request_id: str, - title: str | None = None, - body: str | None = None, - state: PullRequestState | None = None, - ) -> ActionResult[PullRequest]: - data: dict[str, Any] = {} - if title is not None: - data["title"] = title - if body is not None: - data["body"] = body - if state is not None: - data["state"] = state - response = self.client.patch( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}", data=data - ) - return map_action(response, map_pull_request) - - def request_review(self, pull_request_id: str, reviewers: list[str]) -> None: - self.client.post( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}/requested_reviewers", - data={"reviewers": reviewers}, - ) - - def create_review_comment_file( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: - """Leave a review comment on a file.""" - response = self.client.post( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}/comments", - data={ - "body": body, - "commit_id": commit_id, - "path": path, - "side": side, - "subject_type": "file", - }, - ) - return map_action(response, map_review_comment) - - # create_review_comment_line: not supported - # create_review_comment_multiline: not supported - - def create_review_comment_reply( - self, - pull_request_id: str, - body: str, - comment_id: str, - ) -> ActionResult[ReviewComment]: - """Leave a review comment in reply to another review comment.""" - response = self.client.post( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}/comments", - data={ - "body": body, - "in_reply_to": int(comment_id), - }, - ) - return map_action(response, map_review_comment) - - def create_review( - self, - pull_request_id: str, - commit_sha: SHA, - event: ReviewEvent, - comments: list[ReviewCommentInput], - body: str | None = None, - ) -> ActionResult[Review]: - data: dict[str, Any] = { - "commit_id": commit_sha, - "event": GITHUB_REVIEW_EVENT_MAP[event], - "comments": comments, - } - if body is not None: - data["body"] = body - response = self.client.post( - f"/repos/{self.repository['name']}/pulls/{pull_request_id}/reviews", - data=data, - ) - return map_action(response, map_review) - - def create_check_run( - self, - name: str, - head_sha: SHA, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - external_id: str | None = None, - started_at: str | None = None, - completed_at: str | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: - data: dict[str, Any] = { - "name": name, - "head_sha": head_sha, - } - if status is not None: - data["status"] = GITHUB_STATUS_WRITE_MAP[status] - if conclusion is not None: - data["conclusion"] = GITHUB_CONCLUSION_WRITE_MAP[conclusion] - if external_id is not None: - data["external_id"] = external_id - if started_at is not None: - data["started_at"] = started_at - if completed_at is not None: - data["completed_at"] = completed_at - if output is not None: - data["output"] = output - response = self.client.post( - f"/repos/{self.repository['name']}/check-runs", - data=data, - ) - return map_action(response, map_check_run) - - def get_check_run( - self, - check_run_id: ResourceId, - request_options: RequestOptions | None = None, - ) -> ActionResult[CheckRun]: - response = self.client.get( - f"/repos/{self.repository['name']}/check-runs/{check_run_id}", - request_options=request_options, - ) - return map_action(response, map_check_run) - - def update_check_run( - self, - check_run_id: ResourceId, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: - data: dict[str, Any] = {} - if status is not None: - data["status"] = GITHUB_STATUS_WRITE_MAP[status] - if conclusion is not None: - data["conclusion"] = GITHUB_CONCLUSION_WRITE_MAP[conclusion] - if output is not None: - data["output"] = output - response = self.client.patch( - f"/repos/{self.repository['name']}/check-runs/{check_run_id}", - data=data, - ) - return map_action(response, map_check_run) - - def get_archive_link( - self, - ref: str, - archive_format: ArchiveFormat = "tarball", - request_options: RequestOptions | None = None, - ) -> ActionResult[ArchiveLink]: - response = self.client.get( - f"/repos/{self.repository['name']}/{GITHUB_ARCHIVE_FORMAT_MAP[archive_format]}/{ref}", - request_options=request_options, - allow_redirects=False, - ) - if response.status_code != 302 or "Location" not in response.headers: - raise ApiError.from_response(response) - - return { - "data": ArchiveLink(url=response.headers["Location"], headers={}), - "type": "github", - "raw": {"data": response.headers["Location"], "headers": dict(response.headers)}, - "meta": _extract_response_meta(response), - } - - def minimize_comment(self, comment_node_id: str, reason: str) -> None: - self.client.graphql( - MINIMIZE_COMMENT_MUTATION, - {"commentId": comment_node_id, "reason": reason}, - ) - - # resolve_review_thread: not supported - - -def map_author(raw_user: dict[str, Any] | None) -> Author | None: - if raw_user is None: - return None - return Author(id=str(raw_user["id"]), username=raw_user["login"]) - - -def map_comment(raw: dict[str, Any]) -> Comment: - return Comment( - id=str(raw["id"]), - body=raw["body"], - author=map_author(raw.get("user")), - ) - - -def map_reaction(raw: dict[str, Any]) -> ReactionResult: - return ReactionResult( - id=str(raw["id"]), - content=raw["content"], - author=map_author(raw.get("user")), - ) - - -def map_git_blob(raw: dict[str, Any]) -> GitBlob: - return GitBlob(sha=raw["sha"]) - - -def map_file_content(raw: dict[str, Any]) -> FileContent: - return FileContent( - path=raw["path"], - sha=raw["sha"], - content=raw.get("content", ""), - encoding=raw.get("encoding", ""), - size=raw["size"], - ) - - -def map_commit_author(raw_author: dict[str, Any] | None) -> CommitAuthor | None: - if raw_author is None: - return None - - raw_date = raw_author.get("date") - date = datetime.fromisoformat(raw_date) if raw_date else None - - return CommitAuthor( - name=raw_author.get("name", ""), - email=raw_author.get("email", ""), - date=date, - ) - - -_VALID_FILE_STATUSES: set[str] = { - "added", - "removed", - "modified", - "renamed", - "copied", - "changed", - "unchanged", -} - - -def map_commit_file(raw_file: dict[str, Any]) -> CommitFile: - raw_status = raw_file.get("status", "modified") - status = raw_status if raw_status in _VALID_FILE_STATUSES else "unknown" - return CommitFile( - filename=raw_file["filename"], - status=cast(FileStatus, status), - patch=raw_file.get("patch"), - ) - - -def map_commit(raw: dict[str, Any]) -> Commit: - commit = raw.get("commit", {}) - return Commit( - id=raw["sha"], - message=commit.get("message", ""), - author=map_commit_author(commit.get("author")), - files=[map_commit_file(f) for f in raw.get("files", [])], - ) - - -def map_tree_entry(raw_entry: dict[str, Any]) -> TreeEntry: - return TreeEntry( - path=raw_entry["path"], - mode=raw_entry["mode"], - type=raw_entry["type"], - sha=raw_entry["sha"], - size=raw_entry.get("size"), - ) - - -def map_git_tree(raw: dict[str, Any]) -> GitTree: - """Transform a full git tree API response (from create_git_tree).""" - return GitTree( - sha=raw["sha"], - tree=[map_tree_entry(e) for e in raw["tree"]], - truncated=raw["truncated"], - ) - - -def map_git_commit_object(raw: dict[str, Any]) -> GitCommitObject: - return GitCommitObject( - sha=raw["sha"], - tree=GitCommitTree(sha=raw["tree"]["sha"]), - message=raw.get("message", ""), - ) - - -def map_review_comment(raw: dict[str, Any]) -> ReviewComment: - return ReviewComment( - id=str(raw["id"]), - html_url=raw["html_url"], - path=raw["path"], - body=raw["body"], - ) - - -def map_review(raw: dict[str, Any]) -> Review: - return Review( - id=str(raw["id"]), - html_url=raw["html_url"], - ) - - -def map_check_run(raw: dict[str, Any]) -> CheckRun: - raw_status = raw.get("status", "") - raw_conclusion = raw.get("conclusion") - return CheckRun( - id=str(raw["id"]), - name=raw.get("name", ""), - status=GITHUB_STATUS_MAP.get(raw_status, "pending"), - conclusion=GITHUB_CONCLUSION_MAP.get(raw_conclusion) if raw_conclusion else None, - html_url=raw.get("html_url", ""), - ) - - -def map_pull_request_file(raw_file: dict[str, Any]) -> PullRequestFile: - raw_status = raw_file.get("status", "modified") - status = raw_status if raw_status in _VALID_FILE_STATUSES else "unknown" - return PullRequestFile( - filename=raw_file["filename"], - status=cast(FileStatus, status), - patch=raw_file.get("patch"), - changes=raw_file.get("changes", 0), - sha=raw_file.get("sha", ""), - previous_filename=raw_file.get("previous_filename"), - ) - - -def map_pull_request_commit(raw: dict[str, Any]) -> PullRequestCommit: - raw_author = raw.get("commit", {}).get("author") - return PullRequestCommit( - sha=raw["sha"], - message=raw.get("commit", {}).get("message", ""), - author=map_commit_author(raw_author), - ) - - -def map_pull_request(raw: dict[str, Any]) -> PullRequest: - return PullRequest( - id=str(raw["id"]), - number=str(raw["number"]), - title=raw["title"], - body=raw.get("body"), - state=raw["state"], - merged=raw.get("merged_at") is not None, - html_url=raw.get("html_url", ""), - head=PullRequestBranch(sha=raw["head"]["sha"], ref=raw["head"]["ref"]), - base=PullRequestBranch(sha=raw["base"]["sha"], ref=raw["base"]["ref"]), - ) - - -def map_action[T]( - response: requests.Response, fn: Callable[[dict[str, Any]], T] -) -> ActionResult[T]: - raw = response.json() - return { - "data": fn(raw), - "type": "github", - "raw": {"data": raw, "headers": dict(response.headers)}, - "meta": _extract_response_meta(response), - } - - -def map_paginated_action[T]( - pagination: PaginationParams | None, - response: requests.Response, - fn: Callable[[Any], list[T]], -) -> PaginatedActionResult[T]: - raw = response.json() - meta: PaginatedResponseMeta = { - **_extract_response_meta(response), - "next_cursor": str(int(pagination["cursor"]) + 1 if pagination else 2), - } - return { - "data": fn(raw), - "type": "github", - "raw": {"data": raw, "headers": dict(response.headers)}, - "meta": meta, - } diff --git a/src/sentry/scm/private/providers/gitlab.py b/src/sentry/scm/private/providers/gitlab.py deleted file mode 100644 index 4049057177ea39..00000000000000 --- a/src/sentry/scm/private/providers/gitlab.py +++ /dev/null @@ -1,742 +0,0 @@ -""" -GitLab service provider module. - -Unsupported actions: - - * create_check_run - * create_git_blob - * create_git_commit - * create_git_tree - * create_pull_request_draft - * create_review - * get_check_run - * get_pull_request_diff - * minimize_comment - * request_review - * resolve_review_thread - * update_branch - * update_check_run -""" - -import datetime -import functools -from collections.abc import Callable -from typing import Any, Iterable -from urllib.parse import urlencode - -from sentry.integrations.gitlab.client import GitLabApiClient -from sentry.integrations.gitlab.utils import GitLabApiClientPath -from sentry.scm.errors import SCMCodedError, SCMProviderException -from sentry.scm.types import ( - SHA, - ActionResult, - ArchiveFormat, - ArchiveLink, - Author, - BranchName, - Comment, - Commit, - CommitAuthor, - FileContent, - GitCommitObject, - GitCommitTree, - GitRef, - GitTree, - PaginatedActionResult, - PaginatedResponseMeta, - PaginationParams, - PullRequest, - PullRequestBranch, - PullRequestCommit, - PullRequestFile, - PullRequestState, - Reaction, - ReactionResult, - Referrer, - Repository, - RequestOptions, - ReviewComment, - ReviewSide, - TreeEntry, -) -from sentry.shared_integrations.exceptions import ApiError - -AWARD_NAME_BY_REACTION: dict[Reaction, str] = { - "+1": "thumbsup", - "-1": "thumbsdown", - "laugh": "laughing", - "confused": "confused", - "heart": "heart", - "hooray": "tada", - "rocket": "rocket", - "eyes": "eyes", -} - -REACTION_BY_AWARD_NAME: dict[str, Reaction] = { - award: reaction for reaction, award in AWARD_NAME_BY_REACTION.items() -} - -GITLAB_ARCHIVE_FORMAT_MAP: dict[ArchiveFormat, str] = { - "tarball": ".tar.gz", - "zip": ".zip", -} - -PULL_REQUEST_STATE_RETRIEVE_MAP: dict[PullRequestState, list[str]] = { - "open": ["opened"], - "closed": ["closed", "merged"], -} -PULL_REQUEST_STATE_UPDATE_MAP: dict[PullRequestState, str] = {"open": "reopen", "closed": "close"} - - -def catch_provider_exception(fn): - @functools.wraps(fn) - def wrapper(*args, **kwargs): - try: - return fn(*args, **kwargs) - except ApiError as e: - raise SCMProviderException(str(e)) from e - - return wrapper - - -class GitLabProvider: - def __init__( - self, client: GitLabApiClient, organization_id: int, repository: Repository - ) -> None: - self.client = client - self.organization_id = organization_id - self.repository = repository - external_id = repository["external_id"] - # External ID format is "{netloc}:{repo_id}", where netloc might contain a colon before a port number - if external_id is None or ":" not in external_id: - raise SCMCodedError(code="malformed_external_id") - self._repo_id = external_id.rsplit(":", maxsplit=1)[1] - - def is_rate_limited(self, referrer: Referrer) -> bool: - return False # Rate-limits temporarily disabled. - - @catch_provider_exception - def get_issue_comments( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: - raw = self.client.get_issue_notes(self._repo_id, issue_id) - return make_paginated_result(map_comment, raw) - - @catch_provider_exception - def create_issue_comment(self, issue_id: str, body: str) -> ActionResult[Comment]: - raw = self.client.create_comment(self._repo_id, issue_id, {"body": body}) - return make_result(map_comment, raw) - - @catch_provider_exception - def delete_issue_comment(self, issue_id: str, comment_id: str) -> None: - self.client.delete_issue_note(self._repo_id, issue_id, comment_id) - - @catch_provider_exception - def get_pull_request( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[PullRequest]: - raw = self.client.get_merge_request(self._repo_id, pull_request_id) - return make_result(map_pull_request, raw) - - @catch_provider_exception - def get_pull_request_comments( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: - """ - To achieve a behavior consistent with GitHub, we filter out: - - 1) GitLab's "system notes" - They are auto-generated comments for events like "Someone resolved all threads". - They don't exist on GitHub and have little use outside GitLab's UI. - - 2) GitLab's review comments - They correspond to GitHub's review comments, which are not returned by GitHub's - "list review comments" endpoint, used to to implement `get_pull_request_comments`. - """ - - raw = self.client.get_merge_request_notes(self._repo_id, pull_request_id) - return make_paginated_result( - map_comment, - raw, - raw_items=( - note - for note in raw - if ( - not note["system"] # Filter out system notes - and note.get("position") is None # Filter out review comments - ) - ), - ) - - @catch_provider_exception - def create_pull_request_comment(self, pull_request_id: str, body: str) -> ActionResult[Comment]: - raw = self.client.create_merge_request_note(self._repo_id, pull_request_id, {"body": body}) - return make_result(map_comment, raw) - - @catch_provider_exception - def delete_pull_request_comment(self, pull_request_id: str, comment_id: str) -> None: - self.client.delete_merge_request_note(self._repo_id, pull_request_id, comment_id) - - @catch_provider_exception - def get_issue_comment_reactions( - self, - issue_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - raw = self.client.get_issue_note_awards(self._repo_id, issue_id, comment_id) - return make_paginated_result( - map_reaction_result, - raw, - raw_items=(award for award in raw if award["name"] in REACTION_BY_AWARD_NAME), - ) - - @catch_provider_exception - def create_issue_comment_reaction( - self, - issue_id: str, - comment_id: str, - reaction: Reaction, - ) -> ActionResult[ReactionResult]: - raw = self.client.create_issue_note_award( - self._repo_id, - issue_id, - comment_id, - AWARD_NAME_BY_REACTION[reaction], - ) - return make_result(map_reaction_result, raw) - - @catch_provider_exception - def delete_issue_comment_reaction( - self, - issue_id: str, - comment_id: str, - reaction_id: str, - ) -> None: - self.client.delete_issue_note_award(self._repo_id, issue_id, comment_id, reaction_id) - - @catch_provider_exception - def get_pull_request_comment_reactions( - self, - pull_request_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - raw = self.client.get_merge_request_note_awards(self._repo_id, pull_request_id, comment_id) - return make_paginated_result( - map_reaction_result, - raw, - raw_items=(award for award in raw if award["name"] in REACTION_BY_AWARD_NAME), - ) - - @catch_provider_exception - def create_pull_request_comment_reaction( - self, - pull_request_id: str, - comment_id: str, - reaction: Reaction, - ) -> ActionResult[ReactionResult]: - raw = self.client.create_merge_request_note_award( - self._repo_id, pull_request_id, comment_id, AWARD_NAME_BY_REACTION[reaction] - ) - return make_result(map_reaction_result, raw) - - @catch_provider_exception - def delete_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction_id: str - ) -> None: - self.client.delete_merge_request_note_award( - self._repo_id, - pull_request_id, - comment_id, - reaction_id, - ) - - @catch_provider_exception - def get_issue_reactions( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - raw = self.client.get_issue_awards(self._repo_id, issue_id) - return make_paginated_result( - map_reaction_result, - raw, - raw_items=(award for award in raw if award["name"] in REACTION_BY_AWARD_NAME), - ) - - @catch_provider_exception - def create_issue_reaction( - self, issue_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - raw = self.client.create_issue_award( - self._repo_id, - issue_id, - AWARD_NAME_BY_REACTION[reaction], - ) - return make_result(map_reaction_result, raw) - - @catch_provider_exception - def delete_issue_reaction(self, issue_id: str, reaction_id: str) -> None: - self.client.delete_issue_award(self._repo_id, issue_id, reaction_id) - - @catch_provider_exception - def get_pull_request_reactions( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - raw = self.client.get_merge_request_awards(self._repo_id, pull_request_id) - return make_paginated_result( - map_reaction_result, - raw, - raw_items=(award for award in raw if award["name"] in REACTION_BY_AWARD_NAME), - ) - - @catch_provider_exception - def create_pull_request_reaction( - self, pull_request_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - raw = self.client.create_merge_request_award( - self._repo_id, pull_request_id, AWARD_NAME_BY_REACTION[reaction] - ) - return make_result(map_reaction_result, raw) - - @catch_provider_exception - def delete_pull_request_reaction(self, pull_request_id: str, reaction_id: str) -> None: - self.client.delete_merge_request_award(self._repo_id, pull_request_id, reaction_id) - - @catch_provider_exception - def get_branch( - self, - branch: BranchName, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitRef]: - raw = self.client.get_branch(self._repo_id, branch) - return make_result(map_git_ref, raw) - - @catch_provider_exception - def create_branch(self, branch: BranchName, sha: SHA) -> ActionResult[GitRef]: - raw = self.client.create_branch(self._repo_id, branch, sha) - return make_result(map_git_ref, raw) - - @catch_provider_exception - def get_tree( - self, - tree_sha: SHA, - recursive: bool = True, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitTree]: - """List the repository tree at a given ref. - - GitLab's tree API takes a ref (commit SHA, branch, tag) rather than a - tree-object SHA. We treat ``tree_sha`` as a ref so callers can pass a - commit SHA obtained from ``get_git_commit``. - """ - raw = self.client.get_repository_tree(self._repo_id, ref=tree_sha, recursive=recursive) - return ActionResult( - data=GitTree( - sha=tree_sha, - tree=[map_tree_entry(e) for e in raw], - truncated=False, - ), - type="gitlab", - raw={"data": raw, "headers": None}, - meta={}, - ) - - @catch_provider_exception - def get_git_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitCommitObject]: - """Get a commit as a git object. - - GitLab's commit endpoint does not expose the tree-object SHA. We set - ``tree.sha`` to the commit SHA so that downstream code can pass it to - ``get_tree`` (which accepts any ref). - """ - raw = self.client.get_commit(self._repo_id, sha) - return make_result(map_git_commit_object, raw) - - @catch_provider_exception - def get_archive_link( - self, - ref: str, - archive_format: ArchiveFormat = "tarball", - ) -> ActionResult[ArchiveLink]: - fmt = GITLAB_ARCHIVE_FORMAT_MAP[archive_format] - path = GitLabApiClientPath.archive.format(project=self._repo_id, format=fmt) - url = GitLabApiClientPath.build_api_url(self.client.base_url, path) - if ref: - url = f"{url}?{urlencode({'sha': ref})}" - token_data = self.client.get_access_token() - token = token_data["access_token"] if token_data else None - data = ArchiveLink(url=url, headers={"Authorization": f"Bearer {token}"} if token else {}) - return ActionResult( - data=data, - type="gitlab", - raw={"data": url, "headers": None}, - meta={}, - ) - - @catch_provider_exception - def get_file_content( - self, - path: str, - ref: str | None = None, - request_options: RequestOptions | None = None, - ) -> ActionResult[FileContent]: - raw = self.client.get_file_content(self._repo_id, path, ref) - return make_result(map_file_content, raw) - - @catch_provider_exception - def get_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[Commit]: - raw = self.client.get_commit(self._repo_id, sha) - return make_result(map_commit, raw) - - @catch_provider_exception - def get_commits( - self, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - raw = self.client.get_commits(self._repo_id, ref=ref, path=None) - return make_paginated_result(map_commit, raw) - - @catch_provider_exception - def get_commits_by_path( - self, - path: str, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - raw = self.client.get_commits(self._repo_id, ref=ref, path=path) - return make_paginated_result(map_commit, raw) - - @catch_provider_exception - def compare_commits( - self, - start_sha: SHA, - end_sha: SHA, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - raw = self.client.compare_commits(self._repo_id, start_sha, end_sha) - return make_paginated_result(map_commit, raw, raw_items=raw["commits"]) - - @catch_provider_exception - def get_pull_request_files( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestFile]: - raw = self.client.get_merge_request_diffs(self._repo_id, pull_request_id) - return make_paginated_result(map_pull_request_file, raw) - - @catch_provider_exception - def get_pull_request_commits( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestCommit]: - raw = self.client.get_merge_request_commits(self._repo_id, pull_request_id) - return make_paginated_result(map_pull_request_commit, raw, raw_items=reversed(raw)) - - @catch_provider_exception - def get_pull_requests( - self, - state: PullRequestState | None = "open", - # @todo The 'head' parameter has very ad-hoc behavior on GitHub; we should consider removing it entirely. - head: BranchName | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequest]: - raw = [] - gitlab_states: list[str] | list[None] - if state: - gitlab_states = PULL_REQUEST_STATE_RETRIEVE_MAP[state] - else: - gitlab_states = [None] - for gitlab_state in gitlab_states: - raw.extend(self.client.get_merge_requests(self._repo_id, state=gitlab_state)) - return make_paginated_result(map_pull_request, raw) - - @catch_provider_exception - def create_pull_request( - self, - title: str, - body: str, - head: str, - base: str, - ) -> ActionResult[PullRequest]: - data = { - "title": title, - "description": body, - "source_branch": head, - "target_branch": base, - } - raw = self.client.create_merge_request(self._repo_id, data) - return make_result(map_pull_request, raw) - - @catch_provider_exception - def update_pull_request( - self, - pull_request_id: str, - title: str | None = None, - body: str | None = None, - state: PullRequestState | None = None, - ) -> ActionResult[PullRequest]: - data = {} - if title is not None: - data["title"] = title - if body is not None: - data["description"] = body - if state is not None: - data["state_event"] = PULL_REQUEST_STATE_UPDATE_MAP[state] - raw = self.client.update_merge_request(self._repo_id, pull_request_id, data) - return make_result(map_pull_request, raw) - - @catch_provider_exception - def create_review_comment_file( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: - """ - GitLab's "notes" are similar to GitHub's "comments". - Additionally, each note belongs to a "discussion". - - On GitLab, one replies to a discussion. On GitHub, one replies to a comment. - - To allow replying to review comments in a consistent way across providers, - we build a comment ID made of the GitLab's discussion ID and comment ID. - It can be passed to `create_review_comment_reply`, and uniquely identifies a note. - """ - - versions = self.client.get_merge_request_versions(self._repo_id, pull_request_id) - raw = self.client.create_merge_request_discussion( - self._repo_id, - pull_request_id, - { - "body": body, - "position": { - "position_type": "file", - "base_sha": versions[0]["base_commit_sha"], - "head_sha": versions[0]["head_commit_sha"], - "start_sha": versions[0]["start_commit_sha"], - "new_path": path, - "old_path": path, - }, - }, - ) - return make_result( - map_review_comment(raw["id"]), - raw, - raw_item=raw["notes"][0], - ) - - @catch_provider_exception - def create_review_comment_reply( - self, - pull_request_id: str, - body: str, - comment_id: str, - ) -> ActionResult[ReviewComment]: - """ - The comment_id must have the format returned by `create_review_comment_file`. - The newly created comment's ID will have the same format. - """ - discussion_id = comment_id.split(":")[0] - raw = self.client.create_merge_request_discussion_note( - self._repo_id, - pull_request_id, - discussion_id, - {"body": body}, - ) - return make_result( - map_review_comment(discussion_id), - raw, - ) - - -def make_paginated_result[T]( - map_item: Callable[[dict[str, Any]], T], - raw: Any, - *, - raw_items: Iterable[dict[str, Any]] | None = None, -) -> PaginatedActionResult[T]: - if raw_items is None: - assert isinstance(raw, list) - raw_items = raw - return PaginatedActionResult( - data=[map_item(item) for item in raw_items], - type="gitlab", - raw={"data": raw, "headers": None}, - # No actual pagination for now - meta=PaginatedResponseMeta(next_cursor=None), - ) - - -def make_result[T]( - map_item: Callable[[dict[str, Any]], T], - raw: Any, - *, - raw_item: dict[str, Any] | None = None, -) -> ActionResult[T]: - if raw_item is None: - assert isinstance(raw, dict) - raw_item = raw - return ActionResult( - data=map_item(raw_item), - type="gitlab", - raw={"data": raw, "headers": None}, - meta={}, - ) - - -def map_author(raw: dict[str, Any]) -> Author: - return Author( - id=str(raw["id"]), - username=raw["username"], - ) - - -def map_comment(raw: dict[str, Any]) -> Comment: - return Comment( - id=str(raw["id"]), - body=raw["body"], - author=map_author(raw["author"]), - ) - - -def map_commit(raw: dict[str, Any]) -> Commit: - return Commit( - id=str(raw["id"]), - message=raw["message"], - author=CommitAuthor( - name=raw["author_name"], - email=raw["author_email"], - date=datetime.datetime.fromisoformat(raw["created_at"]), - ), - files=None, - ) - - -def map_file_content(raw: dict[str, Any]) -> FileContent: - return FileContent( - path=raw["file_path"], - sha=raw["blob_id"], - content=raw["content"], - encoding=raw["encoding"], - size=raw["size"], - ) - - -def map_git_ref(raw: dict[str, Any]) -> GitRef: - return GitRef(ref=raw["name"], sha=raw["commit"]["id"]) - - -def map_pull_request(raw: dict[str, Any]) -> PullRequest: - return PullRequest( - id=str(raw["id"]), - number=str(raw["iid"]), - title=raw["title"], - body=raw["description"] or None, - state="open" if raw["state"] == "opened" else "closed", - base=PullRequestBranch(ref=raw["target_branch"], sha=None), - head=PullRequestBranch( - ref=raw["source_branch"], - sha=raw["sha"], - ), - merged=raw["merged_at"] is not None, - html_url=raw["web_url"], - ) - - -def map_pull_request_commit(raw: dict[str, Any]) -> PullRequestCommit: - return PullRequestCommit( - sha=raw["id"], - message=raw["message"], - author=CommitAuthor( - name=raw["author_name"], - email=raw["author_email"], - date=datetime.datetime.fromisoformat(raw["authored_date"]), - ), - ) - - -def map_pull_request_file(raw: dict[str, Any]) -> PullRequestFile: - return PullRequestFile( - filename=raw["new_path"], - previous_filename=(raw["old_path"] if raw["old_path"] != raw["new_path"] else None), - status=("added" if raw["new_file"] else "removed" if raw["deleted_file"] else "modified"), - changes=0, - patch=raw.get("diff"), - sha="", - ) - - -def map_reaction_result(raw: dict[str, Any]) -> ReactionResult: - return ReactionResult( - id=str(raw["id"]), - content=REACTION_BY_AWARD_NAME[raw["name"]], - author=map_author(raw["user"]), - ) - - -def map_git_commit_object(raw: dict[str, Any]) -> GitCommitObject: - return GitCommitObject( - sha=raw["id"], - # GitLab's commit API does not return a tree-object SHA. We use the - # commit SHA so callers can pass it to get_tree (which accepts any ref). - tree=GitCommitTree(sha=raw["id"]), - message=raw["message"], - ) - - -def map_tree_entry(raw: dict[str, Any]) -> TreeEntry: - return TreeEntry( - path=raw["path"], - mode=raw["mode"], - type=raw["type"], - sha=raw["id"], - size=None, - ) - - -def map_review_comment(discussion_id: str) -> Callable[[dict[str, Any]], ReviewComment]: - def _map_review_comment(raw: dict[str, Any]) -> ReviewComment: - return ReviewComment( - id=f"{discussion_id}:{raw['id']}", - html_url=None, - path=raw["position"]["new_path"], - body=raw["body"], - ) - - return _map_review_comment diff --git a/src/sentry/scm/private/rate_limit.py b/src/sentry/scm/private/rate_limit.py index 75af64d0ece21e..ac087ae0ad55ac 100644 --- a/src/sentry/scm/private/rate_limit.py +++ b/src/sentry/scm/private/rate_limit.py @@ -1,209 +1,9 @@ -import functools -from typing import Callable, Protocol - from django.conf import settings from redis import RedisError -from sentry.scm.types import Referrer from sentry.utils import redis -def usage_count_key(provider: str, organization_id: int, time_bucket: int, referrer: str) -> str: - return f"rl:scm:{provider}:{organization_id}:{referrer}:{time_bucket}" - - -def total_limit_key(provider: str, organization_id: int) -> str: - return f"limit:scm:{provider}:{organization_id}" - - -class RateLimitProvider(Protocol): - """ - Type definition for rate-limit service providers. Service providers could be Redis, local - in-memory, an RDBMS, or anything really (so long as it persists state between requests). In - practice this will always be Redis but we define the type so we can simplify testing and - simulation. - """ - - def get_and_set_rate_limit( - self, total_key: str, usage_key: str, expiration: int - ) -> tuple[int | None, int]: - """ - Get the request limit and incr/expire quota usage for the key. - - :param total_key: The location of the request limit. - :param usage_key: The location of the quota counter. - :param expiration: The number of seconds until the key expires. - """ - ... - - def get_accounted_usage(self, keys: list[str]) -> int: - """Return the sum of a given set of keys.""" - ... - - def set_key_values(self, kvs: dict[str, tuple[int, int | None]]) -> None: - """For a given set of key, value pairs set them in the Redis Cluster.""" - ... - - -class DynamicRateLimiter: - """ - Quota management class for external rate-limits with dynamic, per-organization request limits. - - The `DynamicRateLimiter` class operates as an eventually consistent mirror of an externally - managed rate limiter. This class defines best-effort load shedding behavior. Because we will - never be consistent with the primary our goal is to reasonably allocate traffic between - referrers. We offer no guarantees that this class will actually reserve quota. It should more - accurately be thought of as a load-shedding heuristic where unallocated referrer requests are - eagerly dropped. - - :param get_time_in_seconds: Get the current UTC timestamp in seconds. - :param organization_id: The organization-id we're scoped to. - :param provider: The service-provider we received rate-limit headers from. - :param rate_limit_window_seconds: The number of seconds in a rate-limit window. - :param referrer_allocation: The referrer allocation pool we're working with. - :param referrer: The referrer being used to make a request. - """ - - def __init__( - self, - get_time_in_seconds: Callable[[], int], - organization_id: int, - provider: str, - rate_limit_provider: RateLimitProvider, - rate_limit_window_seconds: int, - referrer_allocation: dict[Referrer, float], - recorded_capacity: int | None = None, - ) -> None: - self.get_time_in_seconds = get_time_in_seconds - self.organization_id = organization_id - self.provider = provider - self.rate_limit_provider = rate_limit_provider - self.rate_limit_window_seconds = rate_limit_window_seconds - self.referrer_allocation = referrer_allocation - self.recorded_capacity = recorded_capacity - - def is_rate_limited(self, referrer: Referrer) -> bool: - """ - Returns true if the quota for this organization has been exhausted. - - This check is best-effort and is not guaranteed to prevent a rate-limit error response from - a service-provider. - """ - assert referrer == "shared" or referrer in self.referrer_allocation, ( - 'Referrer must exist in the allocation pool. Pass "shared" if no allocation was defined.' - ) - - # Find the bucket ID of the request. The bucket ID is the number of windows which have - # previously elapsed. - current_time = self.get_time_in_seconds() - time_bucket = current_time // self.rate_limit_window_seconds - - # Computed as the window minus the number seconds elapsed within the window. So if our window - # is 100 seconds and 10 seconds of the current window has already elapsed then the remaining - # time is 90 seconds. - expires_in = self.rate_limit_window_seconds - int( - current_time % self.rate_limit_window_seconds - ) - - # Get the total capacity of the service-provider and the amount of quota we've consumed for - # a given referrer. If the referrer does not exist in the allocation pool - service_capacity, quota_used = self.rate_limit_provider.get_and_set_rate_limit( - total_limit_key(self.provider, self.organization_id), - usage_count_key(self.provider, self.organization_id, time_bucket, referrer), - expires_in, - ) - - # If no limit could be found we fail open. We'll populate the limit on the other-side of the - # HTTP request. - if service_capacity is None: - return False - - # Cache this value on the class instance. We'll return back to it later when updating the - # rate-limit metadata. - self.recorded_capacity = service_capacity - - # If the referrer exists in the allocation pool then we compute its capacity otherwise we - # need to compute the total unallocated "shared" capacity. - if referrer == "shared": - referrer_capacity = int( - service_capacity * (1.0 - sum(self.referrer_allocation.values())) - ) - else: - referrer_capacity = int(service_capacity * self.referrer_allocation[referrer]) - - return quota_used >= referrer_capacity - - def update_rate_limit_meta(self, capacity: int, consumed: int, next_window_start: int) -> None: - """ - Some service-providers offer dynamic rate-limits per organization. We need to cache the - metadata service-providers return in their API responses and use that metadata to rate- - limit our own requests eagerly. - - Service-providers will provide their capacity limits and their quota consumption figures. - We use this data to update our internal representation so that we most accurately mirror - true usage figures. If every use of a service-provider transits the SCM this method is - unnecessary. This method only exists because there is direct integration usage. - - :param capacity: The actual total number of requests available per window. - :param consumed: The total number of requests the service-provider is telling us they have received. - :param next_window_start: The next rate-limit window after the current window. - """ - # We need to figure out what window Sentry thinks its in and what window the service-provider - # thinks its in. - current_time = self.get_time_in_seconds() - time_bucket = current_time // self.rate_limit_window_seconds - - # TODO: This might be a little GitHub specific but we don't have another usage example. - specified_bucket = (next_window_start // self.rate_limit_window_seconds) - 1 - - kvs: dict[str, tuple[int, int | None]] = {} - - # If the limit we have recorded in Sentry is different from the rate-limit recording in - # the service-provider we need to update our limit to match. - if self.recorded_capacity != capacity: - kvs[total_limit_key(self.provider, self.organization_id)] = (capacity, None) - - # If we share the same window as the service-provider we can update our rate-limits to match - # what the service-provider recorded. It doesn't matter if we're perfect. - if time_bucket == specified_bucket: - key_fn = functools.partial( - usage_count_key, self.provider, self.organization_id, time_bucket - ) - - # Computed as the window minus the number seconds elapsed within the window. So if our window - # is 100 seconds and 10 seconds of the current window has already elapsed then the remaining - # time is 90 seconds. - expiration = self.rate_limit_window_seconds - int( - current_time % self.rate_limit_window_seconds - ) - - # The shared usage is the delta of the accounted usage and the reported usage. This - # value is expected to be strictly higher than our accounted shared usage because a - # significant portion of Sentry accesses GitHub without passing through the rate-limiter. - # - # We will only throttle usage generated by the SCM using a shared referrer so this is - # strictly worse for early adopters in the shared pool. However, we will hopefully solve - # this problem relatively quickly. - # - # We don't look up our shared usage. It doesn't matter if its non-zero. The shared usage - # is what GitHub says it is. The accounted usage is what we say it is. - # - # TODO: If one day a significant majority of usage of GitHub transits the SCM this can - # go away and we can just set the limit blindly. - try: - accounted_usage = self.rate_limit_provider.get_accounted_usage( - [key_fn(referrer) for referrer in self.referrer_allocation] - ) - kvs[key_fn("shared")] = (max(0, consumed - accounted_usage), expiration) - except IndeterminateResult: - # If we could not fetch the full accounted usage we don't bother updating the - # shared quota value. It can be done on a later iteration. - pass - - if kvs: - self.rate_limit_provider.set_key_values(kvs) - - class RedisRateLimitProvider: def __init__(self): self.cluster = redis.redis_clusters.get(settings.SENTRY_SCM_REDIS_CLUSTER) diff --git a/src/sentry/scm/private/rpc.py b/src/sentry/scm/private/rpc.py deleted file mode 100644 index 86ed393806cd5c..00000000000000 --- a/src/sentry/scm/private/rpc.py +++ /dev/null @@ -1,186 +0,0 @@ -from typing import Any, Callable, cast - -import pydantic - -from sentry.scm.actions import ( - SourceCodeManager, - compare_commits, - create_branch, - create_check_run, - create_git_blob, - create_git_commit, - create_git_tree, - create_issue_comment, - create_issue_comment_reaction, - create_issue_reaction, - create_pull_request, - create_pull_request_comment, - create_pull_request_comment_reaction, - create_pull_request_draft, - create_pull_request_reaction, - create_review, - create_review_comment_file, - create_review_comment_reply, - delete_issue_comment, - delete_issue_comment_reaction, - delete_issue_reaction, - delete_pull_request_comment, - delete_pull_request_comment_reaction, - delete_pull_request_reaction, - get_archive_link, - get_branch, - get_capabilities, - get_check_run, - get_commit, - get_commits, - get_commits_by_path, - get_file_content, - get_git_commit, - get_issue_comment_reactions, - get_issue_comments, - get_issue_reactions, - get_pull_request, - get_pull_request_comment_reactions, - get_pull_request_comments, - get_pull_request_commits, - get_pull_request_diff, - get_pull_request_files, - get_pull_request_reactions, - get_pull_requests, - get_tree, - minimize_comment, - request_review, - update_branch, - update_check_run, - update_pull_request, -) -from sentry.scm.errors import ( - SCMProviderNotSupported, - SCMRpcActionCallError, - SCMRpcActionNotFound, - SCMRpcCouldNotDeserializeRequest, -) -from sentry.scm.types import PROVIDER_SET, ProviderName - - -def dispatch(action_name: str, raw_request_data: dict[str, Any]): - """ - Dispatch an RPC request. - - Action arguments are yolo'd for now. Better type-safety and error messages will be introduced - later. Our dedicated client should make this less of a practical concern. - """ - if action_name not in scm_action_registry: - raise SCMRpcActionNotFound(action_name) - - try: - request = RequestData.parse_obj(raw_request_data) - except pydantic.ValidationError as e: - raise SCMRpcCouldNotDeserializeRequest(e.errors()) from e - - organization_id = request.args.organization_id - - repository_id: int | tuple[str, str] - if isinstance(request.args.repository_id, RequestData.Args.CompositeRepositoryId): - if request.args.repository_id.provider not in PROVIDER_SET: - raise SCMProviderNotSupported( - f"{request.args.repository_id.provider} is not supported." - ) - - repository_id = ( - cast(ProviderName, request.args.repository_id.provider), - request.args.repository_id.external_id, - ) - else: - repository_id = request.args.repository_id - - scm = SourceCodeManager.make_from_repository_id(organization_id, repository_id) - - try: - return scm_action_registry[action_name](scm, **request.args.get_extra_fields()) - except AttributeError as e: - raise SCMProviderNotSupported( - "call_missing_provider_method is not supported by service-provider GitHubProvider" - ) from e - except TypeError as e: - raise SCMRpcActionCallError(action_name, str(e)) from e - - -class RequestData(pydantic.BaseModel, extra=pydantic.Extra.allow): - class Args(pydantic.BaseModel, extra=pydantic.Extra.allow): - organization_id: int - - class CompositeRepositoryId(pydantic.BaseModel, extra=pydantic.Extra.forbid): - provider: str - external_id: str - - repository_id: int | CompositeRepositoryId - - def get_extra_fields(self) -> dict[str, Any]: - return {k: v for k, v in self.__dict__.items() if k not in self.__fields__} - - args: Args - - -scm_action_registry: dict[str, Callable] = { - # These callables must accept a SourceCodeManager as their first argument, - # and then they are free to accept any other **kwargs they want. - # Their return type must be JSON-serializable. - # - # This dict could be populated dynamically by scanning the SourceCodeManager class for methods. - # Explicit listing give us more control: we can rename methods, - # delay exposing them as RPC, adapt their interface, etc. - # - # If a method of SourceCodeManager accepts only JSON-serializable arguments, by names, and - # returns a JSON-serializable type, then it can be listed here directly. - # Else, an adapter function must be used. - "get_capabilities_v1": lambda scm: sorted(get_capabilities(scm)), - "compare_commits_v1": compare_commits, - "create_branch_v1": create_branch, - "create_check_run_v1": create_check_run, - "create_git_blob_v1": create_git_blob, - "create_git_commit_v1": create_git_commit, - "create_git_tree_v1": create_git_tree, - "create_issue_comment_reaction_v1": create_issue_comment_reaction, - "create_issue_comment_v1": create_issue_comment, - "create_issue_reaction_v1": create_issue_reaction, - "create_pull_request_comment_reaction_v1": create_pull_request_comment_reaction, - "create_pull_request_comment_v1": create_pull_request_comment, - "create_pull_request_draft_v1": create_pull_request_draft, - "create_pull_request_reaction_v1": create_pull_request_reaction, - "create_pull_request_v1": create_pull_request, - "create_review_comment_file_v1": create_review_comment_file, - "create_review_comment_reply_v1": create_review_comment_reply, - "create_review_v1": create_review, - "delete_issue_comment_reaction_v1": delete_issue_comment_reaction, - "delete_issue_comment_v1": delete_issue_comment, - "delete_issue_reaction_v1": delete_issue_reaction, - "delete_pull_request_comment_reaction_v1": delete_pull_request_comment_reaction, - "delete_pull_request_comment_v1": delete_pull_request_comment, - "delete_pull_request_reaction_v1": delete_pull_request_reaction, - "get_branch_v1": get_branch, - "get_check_run_v1": get_check_run, - "get_commit_v1": get_commit, - "get_commits_by_path_v1": get_commits_by_path, - "get_commits_v1": get_commits, - "get_file_content_v1": get_file_content, - "get_git_commit_v1": get_git_commit, - "get_issue_comment_reactions_v1": get_issue_comment_reactions, - "get_issue_comments_v1": get_issue_comments, - "get_issue_reactions_v1": get_issue_reactions, - "get_pull_request_comment_reactions_v1": get_pull_request_comment_reactions, - "get_pull_request_comments_v1": get_pull_request_comments, - "get_pull_request_commits_v1": get_pull_request_commits, - "get_pull_request_diff_v1": get_pull_request_diff, - "get_pull_request_files_v1": get_pull_request_files, - "get_pull_request_reactions_v1": get_pull_request_reactions, - "get_pull_request_v1": get_pull_request, - "get_pull_requests_v1": get_pull_requests, - "get_tree_v1": get_tree, - "minimize_comment_v1": minimize_comment, - "request_review_v1": request_review, - "update_branch_v1": update_branch, - "update_check_run_v1": update_check_run, - "update_pull_request_v1": update_pull_request, - "get_archive_link_v1": get_archive_link, -} diff --git a/src/sentry/scm/private/webhooks/github.py b/src/sentry/scm/private/webhooks/github.py index 0addcdc6609133..842dfaff53be64 100644 --- a/src/sentry/scm/private/webhooks/github.py +++ b/src/sentry/scm/private/webhooks/github.py @@ -1,15 +1,17 @@ from typing import Optional import msgspec +from scm.types import ( + CheckRunAction, + CommentAction, + EventTypeHint, + PullRequestAction, +) from sentry.scm.types import ( - CheckRunAction, CheckRunEvent, - CommentAction, CommentEvent, EventType, - EventTypeHint, - PullRequestAction, PullRequestEvent, SubscriptionEvent, ) diff --git a/src/sentry/scm/types.py b/src/sentry/scm/types.py index 3cf9c60df6640d..645694088f51ef 100644 --- a/src/sentry/scm/types.py +++ b/src/sentry/scm/types.py @@ -1,401 +1,16 @@ from dataclasses import dataclass -from datetime import datetime -from typing import Any, Literal, MutableMapping, Protocol, Required, TypedDict, runtime_checkable - -type Action = Literal["check_run", "comment", "pull_request"] -type EventType = "CheckRunEvent" | "CommentEvent" | "PullRequestEvent" -type EventTypeHint = Literal["check_run", "comment", "pull_request"] -type HybridCloudSilo = Literal["control", "region"] - - -type ProviderName = Literal["bitbucket", "github", "github_enterprise", "gitlab"] -"""The SCM provider that owns an integration or repository.""" - -PROVIDER_SET: set[ProviderName] = set(["bitbucket", "github", "github_enterprise", "gitlab"]) - -type ExternalId = str -""" -Identifier whose origin is an external, source-code-management provider. Refers specifically to -the unique identifier of a repository. -""" - -type ResourceId = str -"""An opaque provider-assigned identifier for a resource (pull request, review, check run, etc.). - -Represented as a string to accommodate providers that use non-integer IDs (e.g. GitLab uses -integers but Bitbucket uses UUIDs). Callers should treat this as opaque and not assume numeric -ordering or format. -""" - -type Reaction = Literal["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] -"""Normalized reaction identifiers shared across all SCM providers.""" - -type Referrer = str -""" -Identifies the caller so providers can apply per-referrer rate-limit policies and emit metrics -scoped by referrer. -""" - -type RepositoryId = int | tuple[ProviderName, ExternalId] -"""A repository can be identified by its internal DB id or by a (provider, external_id) pair.""" - -type FileStatus = Literal[ - "added", "removed", "modified", "renamed", "copied", "changed", "unchanged", "unknown" -] -"""The change type applied to a file in a commit or pull request. - -- added: file was created -- removed: file was deleted -- modified: file contents changed -- renamed: file was moved; see previous_filename for the old path -- copied: file was duplicated from another path -- changed: file metadata changed without content change (e.g. mode) -- unchanged: file appears in the diff context but was not modified -- unknown: file status could not be positively identified -""" - -type ArchiveFormat = Literal["tarball", "zip"] -"""Normalized archive format identifiers shared across all SCM providers.""" - - -class ArchiveLink(TypedDict): - """A download URL bundled with the authentication headers required to fetch it.""" - - url: str - headers: dict[str, str] - - -type BuildStatus = Literal["pending", "running", "completed"] -"""The lifecycle stage of a CI build. - -- pending: created or queued but not yet running -- running: actively executing -- completed: finished; see BuildConclusion for the outcome -""" - -type BuildConclusion = Literal[ - "success", - "failure", - "cancelled", - "skipped", - "timed_out", - "neutral", - "action_required", - "unknown", -] -"""The terminal outcome of a completed build. - -- success: all checks passed -- failure: one or more checks failed -- cancelled: stopped before completion -- skipped: deliberately bypassed -- timed_out: exceeded the time limit -- neutral: completed without a pass/fail determination -- action_required: requires manual intervention before proceeding -- unknown: outcome could not be determined -""" - -type TreeEntryMode = Literal["100644", "100755", "040000", "160000", "120000"] -"""UNIX file mode for a git tree entry, as stored in a git tree object. - -- 100644: regular file (non-executable) -- 100755: executable file -- 040000: directory (subtree) -- 160000: git submodule (gitlink) -- 120000: symbolic link -""" - -type TreeEntryType = Literal["blob", "tree", "commit"] -"""The object type stored at a git tree entry. - -- blob: a file -- tree: a directory (subtree) -- commit: a submodule reference -""" - -type ReviewSide = Literal["LEFT", "RIGHT"] -"""Which side of a diff a review comment is anchored to. - -- LEFT: the base (original) side of the diff -- RIGHT: the head (modified) side of the diff -""" - -type BranchName = str -type SHA = str -type PullRequestState = Literal["open", "closed"] -type ReviewEvent = Literal["approve", "change_request", "comment"] - - -class PaginationParams(TypedDict, total=False): - """Controls page traversal for list endpoints. - - - cursor: an opaque token returned from a previous page's `next_cursor` - - per_page: how many items to return per page - """ - - cursor: str - per_page: int - - -class RequestOptions(TypedDict, total=False): - """Transport-level options for single-resource fetches. - - - if_none_match: send an `If-None-Match` header (ETag-based caching) - - if_modified_since: send an `If-Modified-Since` header (UTC datetime) - """ - - if_none_match: str - if_modified_since: datetime - - -class ResponseMeta(TypedDict, total=False): - """Transport-level metadata attached to a single-resource provider response. - - - etag: the `ETag` header value, usable in a subsequent `if_none_match` - - last_modified: UTC datetime parsed from the `Last-Modified` header - """ - - etag: str - last_modified: datetime - - -class PaginatedResponseMeta(TypedDict, total=False): - """Transport-level metadata attached to a paginated provider response. - - Carries all fields from `ResponseMeta` plus a required `next_cursor` - that callers can pass back to `PaginationParams.cursor` to fetch the - next page. `None` means there are no more pages. - """ - - etag: str - last_modified: datetime - next_cursor: Required[str | None] - - -class Author(TypedDict): - """Normalized author identity returned by all SCM providers.""" - - id: ResourceId - username: str - - -class Comment(TypedDict): - """Provider-agnostic representation of an issue or pull-request comment.""" - - id: ResourceId - body: str | None - author: Author | None - - -class ReactionResult(TypedDict): - """Provider-agnostic representation of a reaction on an issue, comment, or pull request.""" - - id: ResourceId - content: Reaction - author: Author | None - - -class PullRequestBranch(TypedDict): - """A branch reference within a pull request (head or base).""" - - sha: SHA | None - ref: BranchName - - -class PullRequest(TypedDict): - """Provider-agnostic representation of a pull request.""" - - # @todo Why do we have two ids here? Confusing. - id: ResourceId - number: str - title: str - body: str | None - state: PullRequestState - merged: bool - html_url: str - head: PullRequestBranch - base: PullRequestBranch - - -class RawResult(TypedDict): - headers: MutableMapping[str, str] | None - data: Any - - -class ActionResult[T](TypedDict): - """Wraps a provider response with metadata and the original API payload. - - Pairs a normalized domain object with the provider name and raw API - payload. This lets callers work with a stable interface while still - having access to provider-specific fields when needed. - - The `meta` field carries transport-level metadata such as ETags. - Pass an empty dict when the provider does not supply any metadata. - """ - - data: T - type: ProviderName - raw: RawResult - meta: ResponseMeta - - -class PaginatedActionResult[T](TypedDict): - """Wraps a paginated provider response. - - Identical to `ActionResult` but carries a `PaginatedResponseMeta` with a required - `page_info`, guaranteeing that callers of list endpoints always have access to pagination - state. - """ - - data: list[T] - type: ProviderName - raw: RawResult - meta: PaginatedResponseMeta - - -class Repository(TypedDict): - """Identifies a repository within a Sentry integration.""" - - integration_id: int - name: str - organization_id: int - is_active: bool - external_id: str | None - - -class GitRef(TypedDict): - """A git reference (branch pointer).""" - - ref: BranchName - sha: SHA - - -class GitBlob(TypedDict): - sha: SHA - - -class FileContent(TypedDict): - path: str - sha: SHA - content: str # base64-encoded - encoding: str - size: int - - -class CommitAuthor(TypedDict): - name: str - email: str - date: datetime | None - - -class CommitFile(TypedDict): - filename: str - status: FileStatus - patch: str | None - - -class Commit(TypedDict): - id: SHA - message: str - author: CommitAuthor | None - files: list[CommitFile] | None - - -class CommitComparison(TypedDict): - ahead_by: int - behind_by: int - commits: list[Commit] - - -class TreeEntry(TypedDict): - path: str - mode: TreeEntryMode - type: TreeEntryType - sha: SHA - size: int | None - - -class InputTreeEntry(TypedDict): - path: str - mode: TreeEntryMode - type: TreeEntryType - sha: SHA | None # None for deletions - - -class GitTree(TypedDict): - sha: SHA - tree: list[TreeEntry] - truncated: bool - - -class GitCommitTree(TypedDict): - sha: SHA - - -class GitCommitObject(TypedDict): - sha: SHA - tree: GitCommitTree - message: str - - -class PullRequestFile(TypedDict): - filename: str - status: FileStatus - patch: str | None - changes: int - sha: SHA - previous_filename: str | None - - -class PullRequestCommit(TypedDict): - sha: SHA - message: str - author: CommitAuthor | None - - -class ReviewCommentInput(TypedDict, total=False): - """Input for an inline comment within a batch review.""" - - path: Required[str] - body: Required[str] - line: int - side: ReviewSide - start_line: int - start_side: ReviewSide - - -class ReviewComment(TypedDict): - """Provider-agnostic representation of a review comment.""" - - id: ResourceId - html_url: str | None - path: str - body: str - - -class Review(TypedDict): - """Provider-agnostic representation of a pull request review.""" - - id: ResourceId - html_url: str - - -class CheckRunOutput(TypedDict, total=False): - """Output annotation for a check run.""" - - title: Required[str] - summary: Required[str] - text: str - - -class CheckRun(TypedDict): - """Provider-agnostic representation of a check run.""" - - id: ResourceId - name: str - status: BuildStatus - conclusion: BuildConclusion | None - html_url: str +from typing import TypedDict + +from scm.types import ( + CheckRunAction, + CheckRunEventData, + CommentAction, + CommentEventData, + CommentType, + ProviderName, + PullRequestAction, + PullRequestEventData, +) class SubscriptionEvent(TypedDict): @@ -468,14 +83,6 @@ class SubscriptionEventSentryMeta(TypedDict): """ -type CheckRunAction = Literal["completed", "created", "requested_action", "rerequested"] - - -class CheckRunEventData(TypedDict): - external_id: str - html_url: str - - @dataclass(frozen=True) class CheckRunEvent: action: CheckRunAction @@ -496,16 +103,6 @@ class CheckRunEvent: """ -type CommentAction = Literal["created", "deleted", "edited", "pinned", "unpinned"] -type CommentType = Literal["issue", "pull_request"] - - -class CommentEventData(TypedDict): - id: str - body: str | None - author: Author | None - - @dataclass(frozen=True) class CommentEvent: """ """ @@ -531,41 +128,6 @@ class CommentEvent: """ -type PullRequestAction = Literal[ - "assigned", - "auto_merge_disabled", - "auto_merge_enabled", - "closed", - "converted_to_draft", - "demilestoned", # Removed a milestone. - "dequeued", # Removed from merge queue. - "edited", - "enqueued", # Added to merge queue. - "labeled", - "locked", - "milestoned", # Added a milestone. - "opened", - "ready_for_review", - "reopened", - "review_request_removed", - "review_requested", - "synchronize", # Commits were pushed. - "unassigned", - "unlabeled", - "unlocked", -] - - -class PullRequestEventData(TypedDict): - id: str - title: str - description: str | None - head: PullRequestBranch - base: PullRequestBranch - is_private_repo: bool - author: Author | None - - @dataclass(frozen=True) class PullRequestEvent: """ @@ -591,575 +153,4 @@ class PullRequestEvent: """ -# Issue Comment Protocols - - -@runtime_checkable -class GetIssueCommentsProtocol(Protocol): - def get_issue_comments( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: ... - - -@runtime_checkable -class CreateIssueCommentProtocol(Protocol): - def create_issue_comment(self, issue_id: str, body: str) -> ActionResult[Comment]: ... - - -@runtime_checkable -class DeleteIssueCommentProtocol(Protocol): - def delete_issue_comment(self, issue_id: str, comment_id: str) -> None: ... - - -# Pull Request Comment Protocols - - -@runtime_checkable -class GetPullRequestCommentsProtocol(Protocol): - def get_pull_request_comments( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: ... - - -@runtime_checkable -class CreatePullRequestCommentProtocol(Protocol): - def create_pull_request_comment( - self, pull_request_id: str, body: str - ) -> ActionResult[Comment]: ... - - -@runtime_checkable -class DeletePullRequestCommentProtocol(Protocol): - def delete_pull_request_comment(self, pull_request_id: str, comment_id: str) -> None: ... - - -# Issue Comment Reaction Protocols - - -@runtime_checkable -class GetIssueCommentReactionsProtocol(Protocol): - def get_issue_comment_reactions( - self, - issue_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreateIssueCommentReactionProtocol(Protocol): - def create_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeleteIssueCommentReactionProtocol(Protocol): - def delete_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction_id: str - ) -> None: ... - - -# Pull Request Comment Reaction Protocols - - -@runtime_checkable -class GetPullRequestCommentReactionsProtocol(Protocol): - def get_pull_request_comment_reactions( - self, - pull_request_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreatePullRequestCommentReactionProtocol(Protocol): - def create_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeletePullRequestCommentReactionProtocol(Protocol): - def delete_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction_id: str - ) -> None: ... - - -# Issue Reaction Protocols - - -@runtime_checkable -class GetIssueReactionsProtocol(Protocol): - def get_issue_reactions( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreateIssueReactionProtocol(Protocol): - def create_issue_reaction( - self, issue_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeleteIssueReactionProtocol(Protocol): - def delete_issue_reaction(self, issue_id: str, reaction_id: str) -> None: ... - - -# Pull Request Reaction Protocols - - -@runtime_checkable -class GetPullRequestReactionsProtocol(Protocol): - def get_pull_request_reactions( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreatePullRequestReactionProtocol(Protocol): - def create_pull_request_reaction( - self, pull_request_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeletePullRequestReactionProtocol(Protocol): - def delete_pull_request_reaction(self, pull_request_id: str, reaction_id: str) -> None: ... - - -# Branch Protocols - - -@runtime_checkable -class GetBranchProtocol(Protocol): - def get_branch( - self, - branch: BranchName, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitRef]: ... - - -@runtime_checkable -class CreateBranchProtocol(Protocol): - def create_branch(self, branch: BranchName, sha: SHA) -> ActionResult[GitRef]: ... - - -@runtime_checkable -class UpdateBranchProtocol(Protocol): - def update_branch( - self, branch: BranchName, sha: SHA, force: bool = False - ) -> ActionResult[GitRef]: ... - - -# Commit Protocols - - -@runtime_checkable -class GetCommitProtocol(Protocol): - def get_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[Commit]: ... - - -@runtime_checkable -class GetCommitsProtocol(Protocol): - def get_commits( - self, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: ... - - -@runtime_checkable -class GetCommitsByPathProtocol(Protocol): - def get_commits_by_path( - self, - path: str, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: ... - - -@runtime_checkable -class CompareCommitsProtocol(Protocol): - def compare_commits( - self, - start_sha: SHA, - end_sha: SHA, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: ... - - -# Pull Request Protocols - - -@runtime_checkable -class GetPullRequestProtocol(Protocol): - def get_pull_request( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class GetPullRequestsProtocol(Protocol): - def get_pull_requests( - self, - state: PullRequestState | None = "open", - head: BranchName | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequest]: ... - - -@runtime_checkable -class GetPullRequestFilesProtocol(Protocol): - def get_pull_request_files( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestFile]: ... - - -@runtime_checkable -class GetPullRequestCommitsProtocol(Protocol): - def get_pull_request_commits( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestCommit]: ... - - -@runtime_checkable -class GetPullRequestDiffProtocol(Protocol): - def get_pull_request_diff( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[str]: ... - - -@runtime_checkable -class CreatePullRequestProtocol(Protocol): - def create_pull_request( - self, - title: str, - body: str, - head: BranchName, - base: BranchName, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class CreatePullRequestDraftProtocol(Protocol): - def create_pull_request_draft( - self, - title: str, - body: str, - head: BranchName, - base: BranchName, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class UpdatePullRequestProtocol(Protocol): - def update_pull_request( - self, - pull_request_id: str, - title: str | None = None, - body: str | None = None, - state: PullRequestState | None = None, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class RequestReviewProtocol(Protocol): - def request_review(self, pull_request_id: str, reviewers: list[str]) -> None: ... - - -# Git Object Protocols - - -@runtime_checkable -class GetTreeProtocol(Protocol): - def get_tree( - self, - tree_sha: SHA, - recursive: bool = True, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitTree]: ... - - -@runtime_checkable -class GetGitCommitProtocol(Protocol): - def get_git_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitCommitObject]: ... - - -@runtime_checkable -class CreateGitBlobProtocol(Protocol): - def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: ... - - -@runtime_checkable -class CreateGitTreeProtocol(Protocol): - def create_git_tree( - self, tree: list[InputTreeEntry], base_tree: SHA | None = None - ) -> ActionResult[GitTree]: ... - - -@runtime_checkable -class CreateGitCommitProtocol(Protocol): - def create_git_commit( - self, message: str, tree_sha: SHA, parent_shas: list[SHA] - ) -> ActionResult[GitCommitObject]: ... - - -# File Content Protocol - - -@runtime_checkable -class GetFileContentProtocol(Protocol): - def get_file_content( - self, - path: str, - ref: str | None = None, - request_options: RequestOptions | None = None, - ) -> ActionResult[FileContent]: ... - - -# Archive Protocols - - -@runtime_checkable -class GetArchiveLinkProtocol(Protocol): - def get_archive_link( - self, - ref: str, - archive_format: ArchiveFormat = "tarball", - ) -> ActionResult[ArchiveLink]: ... - - -# Check Run Protocols - - -@runtime_checkable -class GetCheckRunProtocol(Protocol): - def get_check_run( - self, - check_run_id: ResourceId, - request_options: RequestOptions | None = None, - ) -> ActionResult[CheckRun]: ... - - -@runtime_checkable -class CreateCheckRunProtocol(Protocol): - def create_check_run( - self, - name: str, - head_sha: SHA, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - external_id: str | None = None, - started_at: str | None = None, - completed_at: str | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: ... - - -@runtime_checkable -class UpdateCheckRunProtocol(Protocol): - def update_check_run( - self, - check_run_id: ResourceId, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: ... - - -# Review Protocols - - -@runtime_checkable -class CreateReviewCommentFileProtocol(Protocol): - def create_review_comment_file( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewCommentLineProtocol(Protocol): - def create_review_comment_line( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - line: int, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewCommentMultilineProtocol(Protocol): - def create_review_comment_multiline( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - start_line: int, - start_side: ReviewSide, - end_line: int, - end_side: ReviewSide, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewCommentReplyProtocol(Protocol): - def create_review_comment_reply( - self, - pull_request_id: str, - body: str, - comment_id: str, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewProtocol(Protocol): - def create_review( - self, - pull_request_id: str, - commit_sha: SHA, - event: ReviewEvent, - comments: list[ReviewCommentInput], - body: str | None = None, - ) -> ActionResult[Review]: ... - - -# Moderation Protocols - - -@runtime_checkable -class MinimizeCommentProtocol(Protocol): - def minimize_comment(self, comment_node_id: str, reason: str) -> None: ... - - -@runtime_checkable -class ResolveReviewThreadProtocol(Protocol): - def resolve_review_thread(self, thread_node_id: str) -> None: ... - - -ALL_PROTOCOLS = ( - CompareCommitsProtocol, - CreateBranchProtocol, - CreateCheckRunProtocol, - CreateGitBlobProtocol, - CreateGitCommitProtocol, - CreateGitTreeProtocol, - CreateIssueCommentProtocol, - CreateIssueCommentReactionProtocol, - CreateIssueReactionProtocol, - CreatePullRequestCommentProtocol, - CreatePullRequestCommentReactionProtocol, - CreatePullRequestDraftProtocol, - CreatePullRequestProtocol, - CreatePullRequestReactionProtocol, - CreateReviewCommentFileProtocol, - CreateReviewCommentLineProtocol, - CreateReviewCommentMultilineProtocol, - CreateReviewCommentReplyProtocol, - CreateReviewProtocol, - DeleteIssueCommentProtocol, - DeleteIssueCommentReactionProtocol, - DeleteIssueReactionProtocol, - DeletePullRequestCommentProtocol, - DeletePullRequestCommentReactionProtocol, - DeletePullRequestReactionProtocol, - GetArchiveLinkProtocol, - GetBranchProtocol, - GetCheckRunProtocol, - GetCommitProtocol, - GetCommitsByPathProtocol, - GetCommitsProtocol, - GetFileContentProtocol, - GetGitCommitProtocol, - GetIssueCommentReactionsProtocol, - GetIssueCommentsProtocol, - GetIssueReactionsProtocol, - GetPullRequestCommentReactionsProtocol, - GetPullRequestCommentsProtocol, - GetPullRequestCommitsProtocol, - GetPullRequestDiffProtocol, - GetPullRequestFilesProtocol, - GetPullRequestProtocol, - GetPullRequestReactionsProtocol, - GetPullRequestsProtocol, - GetTreeProtocol, - MinimizeCommentProtocol, - RequestReviewProtocol, - ResolveReviewThreadProtocol, - UpdateBranchProtocol, - UpdateCheckRunProtocol, - UpdatePullRequestProtocol, -) - - -class Provider(Protocol): - """ - Providers abstract over an integration. They map generic commands to service-provider specific - commands and they map the results of those commands to generic result-types. - - Providers necessarily offer a larger API surface than what is available in an integration. Some - methods may be duplicates in some providers. This is intentional. Providers capture programmer - intent and translate it into a concrete interface. Therefore, providers provide a large range - of behaviors which may or may not be explicitly defined on a service-provider. - - Providers, also by necessity, offer a smaller API surface than what the SCM platform can - maximally provide. There are simply some operations which can not be adequately translated - between providers. None the less, we want to have a service-agnostic interface. This problem - is solved with capability-object-like system. Capabilities are progressively opted into using - structural sub-typing. As a provider's surface area expands the SourceCodeManager class will - automatically recognize that the provider has a particular capability and return "true" when - handling "can" requests. - """ - - organization_id: int - repository: Repository - - def is_rate_limited(self, referrer: Referrer) -> bool: ... +type EventType = CheckRunEvent | CommentEvent | PullRequestEvent diff --git a/tests/sentry/scm/integration/test_github_provider_integration.py b/tests/sentry/scm/integration/test_github_provider_integration.py deleted file mode 100644 index 6e2bcc0d324a3f..00000000000000 --- a/tests/sentry/scm/integration/test_github_provider_integration.py +++ /dev/null @@ -1,596 +0,0 @@ -from datetime import timedelta -from unittest import mock -from urllib.parse import parse_qs, urlparse - -import pytest -import responses -from django.utils import timezone - -from sentry.constants import ObjectStatus -from sentry.models.repository import Repository as RepositoryModel -from sentry.scm.errors import SCMProviderException -from sentry.scm.private.helpers import ( - map_integration_to_provider, - map_repository_model_to_repository, -) -from sentry.scm.private.providers.github import GitHubProvider -from sentry.testutils.cases import TestCase - -REPO_NAME = "test-org/test-repo" - - -class TestGitHubProviderIntegration(TestCase): - provider: GitHubProvider - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def setUp(self, mock_get_jwt): - super().setUp() - ten_days = timezone.now() + timedelta(days=10) - self.integration = self.create_integration( - organization=self.organization, - provider="github", - name="Test GitHub", - external_id="12345", - metadata={ - "access_token": "12345token", - "expires_at": ten_days.strftime("%Y-%m-%dT%H:%M:%S"), - }, - ) - self.repo_model = RepositoryModel.objects.create( - organization_id=self.organization.id, - name=REPO_NAME, - provider="integrations:github", - external_id="67890", - integration_id=self.integration.id, - status=ObjectStatus.ACTIVE, - ) - self.repository = map_repository_model_to_repository(self.repo_model) - self.provider = map_integration_to_provider( # type: ignore[assignment] - self.organization.id, self.integration, self.repository - ) - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_issue_comments(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/1347/comments", - json=[ - { - "id": 1, - "node_id": "MDEyOklzc3VlQ29tbWVudDE=", - "url": f"https://api.github.com/repos/{REPO_NAME}/issues/comments/1", - "html_url": f"https://github.com/{REPO_NAME}/issues/1347#issuecomment-1", - "body": "Me too", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "type": "User", - "site_admin": False, - }, - "created_at": "2011-04-14T16:00:49Z", - "updated_at": "2011-04-14T16:00:49Z", - "issue_url": f"https://api.github.com/repos/{REPO_NAME}/issues/1347", - "author_association": "COLLABORATOR", - }, - ], - ) - - comments = self.provider.get_issue_comments("1347") - - assert len(comments["data"]) == 1 - assert comments["data"][0]["id"] == "1" - assert comments["data"][0]["body"] == "Me too" - assert comments["data"][0]["author"] is not None - assert comments["data"][0]["author"]["id"] == "1" - assert comments["data"][0]["author"]["username"] == "octocat" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_issue_comments_uses_query_params_for_pagination(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/1347/comments?page=3&per_page=25", - json=[], - ) - - self.provider.get_issue_comments("1347", pagination={"cursor": "3", "per_page": 25}) - - assert len(responses.calls) == 1 - request_url = urlparse(responses.calls[0].request.url) - assert request_url.scheme == "https" - assert request_url.netloc == "api.github.com" - assert request_url.path == f"/repos/{REPO_NAME}/issues/1347/comments" - assert parse_qs(request_url.query) == {"page": ["3"], "per_page": ["25"]} - assert responses.calls[0].request.headers["Accept"] == "application/vnd.github+json" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_create_issue_comment(self, mock_get_jwt): - responses.add( - method=responses.POST, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/1/comments", - status=201, - json={ - "id": 1, - "node_id": "MDEyOklzc3VlQ29tbWVudDE=", - "url": f"https://api.github.com/repos/{REPO_NAME}/issues/comments/1", - "html_url": f"https://github.com/{REPO_NAME}/issues/1#issuecomment-1", - "body": "hello", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "type": "User", - "site_admin": False, - }, - "created_at": "2023-05-23T17:00:00Z", - "updated_at": "2023-05-23T17:00:00Z", - "issue_url": f"https://api.github.com/repos/{REPO_NAME}/issues/1", - "author_association": "COLLABORATOR", - }, - ) - - self.provider.create_issue_comment("1", "hello") - - assert len(responses.calls) == 1 - assert ( - responses.calls[0].request.url - == f"https://api.github.com/repos/{REPO_NAME}/issues/1/comments" - ) - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_pull_request(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/pulls/1347", - json={ - "id": 1, - "node_id": "MDExOlB1bGxSZXF1ZXN0MQ==", - "url": f"https://api.github.com/repos/{REPO_NAME}/pulls/1347", - "html_url": f"https://github.com/{REPO_NAME}/pull/1347", - "number": 1347, - "state": "open", - "locked": False, - "title": "Amazing new feature", - "body": "Please pull these awesome changes in!", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "type": "User", - "site_admin": False, - }, - "head": { - "label": "octocat:new-topic", - "ref": "new-topic", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", - }, - "base": { - "label": "octocat:master", - "ref": "master", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5f", - }, - "merged": False, - "mergeable": True, - "mergeable_state": "clean", - "merged_by": None, - "comments": 10, - "review_comments": 0, - "commits": 3, - "additions": 100, - "deletions": 3, - "changed_files": 5, - }, - ) - - result = self.provider.get_pull_request("1347") - - pr = result["data"] - assert pr["head"]["sha"] == "6dcb09b5b57875f334f61aebed695e2e4193db5e" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_pull_request_comments(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/1/comments", - json=[ - { - "id": 10, - "body": "Great stuff!", - "user": { - "login": "octocat", - "id": 1, - }, - }, - ], - ) - - result = self.provider.get_pull_request_comments("1") - - assert len(result["data"]) == 1 - assert result["data"][0]["id"] == "10" - assert result["data"][0]["body"] == "Great stuff!" - assert result["data"][0]["author"] is not None - assert result["data"][0]["author"]["id"] == "1" - assert result["data"][0]["author"]["username"] == "octocat" - assert result["type"] == "github" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_issue_comment_reactions(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/comments/42/reactions", - json=[ - { - "id": 1, - "user": { - "login": "octocat", - "id": 1, - }, - "content": "+1", - }, - { - "id": 2, - "user": { - "login": "hubot", - "id": 2, - }, - "content": "eyes", - }, - ], - headers={}, - ) - - reactions = self.provider.get_issue_comment_reactions("1", "42") - - assert len(reactions["data"]) == 2 - assert reactions["data"][0]["id"] == "1" - assert reactions["data"][0]["content"] == "+1" - assert reactions["data"][0]["author"] is not None - assert reactions["data"][0]["author"]["username"] == "octocat" - assert reactions["data"][1]["id"] == "2" - assert reactions["data"][1]["content"] == "eyes" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_create_issue_comment_reaction(self, mock_get_jwt): - responses.add( - method=responses.POST, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/comments/42/reactions", - status=201, - json={ - "id": 1, - "node_id": "MDg6UmVhY3Rpb24x", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "type": "User", - "site_admin": False, - }, - "content": "heart", - "created_at": "2016-05-20T20:09:31Z", - }, - ) - - self.provider.create_issue_comment_reaction("1", "42", "heart") - - assert len(responses.calls) == 1 - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_issue_reactions(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/42/reactions", - json=[ - { - "id": 1, - "node_id": "MDg6UmVhY3Rpb24x", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "type": "User", - "site_admin": False, - }, - "content": "heart", - "created_at": "2016-05-20T20:09:31Z", - }, - { - "id": 2, - "node_id": "MDg6UmVhY3Rpb24y", - "user": { - "login": "hubot", - "id": 2, - "node_id": "MDQ6VXNlcjI=", - "avatar_url": "https://github.com/images/error/hubot_happy.gif", - "type": "User", - "site_admin": False, - }, - "content": "+1", - "created_at": "2016-05-20T20:09:31Z", - }, - ], - headers={}, - ) - - reactions = self.provider.get_issue_reactions("42") - - assert len(reactions["data"]) == 2 - assert reactions["data"][0]["id"] == "1" - assert reactions["data"][0]["content"] == "heart" - assert reactions["data"][0]["author"] is not None - assert reactions["data"][0]["author"]["id"] == "1" - assert reactions["data"][0]["author"]["username"] == "octocat" - assert reactions["data"][1]["id"] == "2" - assert reactions["data"][1]["content"] == "+1" - assert reactions["data"][1]["author"] is not None - assert reactions["data"][1]["author"]["id"] == "2" - assert reactions["data"][1]["author"]["username"] == "hubot" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_create_issue_reaction(self, mock_get_jwt): - responses.add( - method=responses.POST, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/42/reactions", - status=201, - json={ - "id": 1, - "node_id": "MDg6UmVhY3Rpb24x", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "type": "User", - "site_admin": False, - }, - "content": "rocket", - "created_at": "2016-05-20T20:09:31Z", - }, - ) - - self.provider.create_issue_reaction("42", "rocket") - - assert len(responses.calls) == 1 - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_api_error_raises_scm_provider_exception(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/42/comments", - status=404, - json={ - "message": "Not Found", - "documentation_url": "https://docs.github.com/rest", - }, - ) - - with pytest.raises(SCMProviderException): - self.provider.get_issue_comments("42") - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_api_500_error_raises_scm_provider_exception(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/42/comments", - status=500, - json={"message": "Internal Server Error"}, - ) - - with pytest.raises(SCMProviderException): - self.provider.get_issue_comments("42") - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_api_403_error_raises_scm_provider_exception(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/pulls/1", - status=403, - json={"message": "Forbidden"}, - ) - - with pytest.raises(SCMProviderException): - self.provider.get_pull_request("1") - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_api_422_error_raises_scm_provider_exception(self, mock_get_jwt): - responses.add( - method=responses.POST, - url=f"https://api.github.com/repos/{REPO_NAME}/pulls", - status=422, - json={"message": "Validation Failed", "errors": [{"message": "head already exists"}]}, - ) - - with pytest.raises(SCMProviderException): - self.provider.create_pull_request( - title="Test", body="body", head="feature", base="main" - ) - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_commit(self, mock_get_jwt): - sha = "abc123def456" - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/commits/{sha}", - json={ - "sha": sha, - "commit": { - "message": "Fix bug", - "author": { - "name": "Test User", - "email": "test@example.com", - "date": "2026-02-04T10:00:00Z", - }, - }, - "files": [ - { - "filename": "src/main.py", - "status": "modified", - "patch": "@@ -1 +1 @@\n-old\n+new", - } - ], - }, - ) - - result = self.provider.get_commit(sha) - - commit = result["data"] - assert commit["id"] == sha - assert commit["message"] == "Fix bug" - assert commit["author"] is not None - assert commit["author"]["name"] == "Test User" - assert commit["files"] is not None - assert len(commit["files"]) == 1 - assert commit["files"][0]["filename"] == "src/main.py" - assert commit["files"][0]["status"] == "modified" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_branch(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/branches/main", - json={ - "name": "main", - "commit": {"sha": "abc123def456"}, - }, - ) - - result = self.provider.get_branch("main") - - ref = result["data"] - assert ref["sha"] == "abc123def456" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_pull_request_files(self, mock_get_jwt): - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/pulls/1/files", - json=[ - { - "filename": "src/main.py", - "status": "modified", - "patch": "@@ -1 +1 @@", - "changes": 2, - "sha": "abc123", - }, - { - "filename": "src/new_file.py", - "status": "added", - "patch": "@@ -0,0 +1 @@\n+new", - "changes": 1, - "sha": "def456", - }, - ], - ) - - result = self.provider.get_pull_request_files("1") - - assert len(result["data"]) == 2 - assert result["data"][0]["filename"] == "src/main.py" - assert result["data"][0]["status"] == "modified" - assert result["data"][1]["filename"] == "src/new_file.py" - assert result["data"][1]["status"] == "added" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_create_pull_request(self, mock_get_jwt): - responses.add( - method=responses.POST, - url=f"https://api.github.com/repos/{REPO_NAME}/pulls", - status=201, - json={ - "id": 1, - "number": 42, - "title": "New Feature", - "body": "Description", - "state": "open", - "merged": False, - "url": f"https://api.github.com/repos/{REPO_NAME}/pulls/42", - "html_url": f"https://github.com/{REPO_NAME}/pull/42", - "head": {"ref": "feature", "sha": "abc123"}, - "base": {"ref": "main", "sha": "def456"}, - }, - ) - - result = self.provider.create_pull_request( - title="New Feature", body="Description", head="feature", base="main" - ) - - pr = result["data"] - assert pr["number"] == "42" - assert pr["title"] == "New Feature" - assert pr["state"] == "open" - assert pr["head"]["ref"] == "feature" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_get_pull_request_comments_with_null_author(self, mock_get_jwt): - """Verify null user is handled correctly.""" - responses.add( - method=responses.GET, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/1/comments", - json=[ - { - "id": 10, - "body": "Ghost comment", - "user": None, - }, - ], - ) - - result = self.provider.get_pull_request_comments("1") - - assert len(result["data"]) == 1 - assert result["data"][0]["author"] is None - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_delete_issue_comment(self, mock_get_jwt): - responses.add( - method=responses.DELETE, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/comments/123", - status=204, - ) - - self.provider.delete_issue_comment("1", "123") - - assert len(responses.calls) == 1 - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_delete_issue_comment_reaction(self, mock_get_jwt): - responses.add( - method=responses.DELETE, - url=f"https://api.github.com/repos/{REPO_NAME}/issues/comments/42/reactions/1", - status=204, - ) - - self.provider.delete_issue_comment_reaction("1", "42", "1") - - assert len(responses.calls) == 1 - - def test_repository_conversion_preserves_fields(self) -> None: - assert self.repository["name"] == REPO_NAME - assert self.repository["organization_id"] == self.organization.id - assert self.repository["integration_id"] == self.integration.id - assert self.repository["is_active"] is True diff --git a/tests/sentry/scm/integration/test_helpers_integration.py b/tests/sentry/scm/integration/test_helpers_integration.py index dc48ef0c1949b3..e54d15eb587ba1 100644 --- a/tests/sentry/scm/integration/test_helpers_integration.py +++ b/tests/sentry/scm/integration/test_helpers_integration.py @@ -1,20 +1,18 @@ from unittest.mock import MagicMock import pytest +from scm.errors import SCMCodedError +from scm.providers.github.provider import GitHubProvider +from scm.types import Repository from sentry.constants import ObjectStatus from sentry.models.repository import Repository as RepositoryModel -from sentry.scm.errors import SCMCodedError, SCMProviderException, SCMUnhandledException from sentry.scm.private.helpers import ( - exec_provider_fn, fetch_repository, fetch_service_provider, - initialize_provider, map_integration_to_provider, map_repository_model_to_repository, ) -from sentry.scm.private.providers.github import GitHubProvider -from sentry.scm.types import Repository from sentry.testutils.cases import TestCase @@ -202,139 +200,3 @@ def _make_provider(is_rate_limited: bool = False): provider = MagicMock() provider.is_rate_limited.return_value = is_rate_limited return provider - - -class TestInitializeProvider(TestCase): - def test_raises_repository_not_found(self) -> None: - with pytest.raises(SCMCodedError) as exc_info: - initialize_provider( - self.organization.id, - 99999, - fetch_repository=lambda _, __: None, - ) - assert exc_info.value.code == "repository_not_found" - - def test_raises_repository_inactive(self) -> None: - repository: Repository = { - "integration_id": 1, - "name": "test-org/test-repo", - "organization_id": self.organization.id, - "is_active": False, - "external_id": None, - } - - with pytest.raises(SCMCodedError) as exc_info: - initialize_provider( - self.organization.id, - 1, - fetch_repository=lambda _, __: repository, - ) - assert exc_info.value.code == "repository_inactive" - - def test_raises_repository_organization_mismatch(self) -> None: - repository = _make_active_repository(organization_id=99999) - - with pytest.raises(SCMCodedError) as exc_info: - initialize_provider( - self.organization.id, - 1, - fetch_repository=lambda _, __: repository, - ) - assert exc_info.value.code == "repository_organization_mismatch" - - def test_raises_integration_not_found(self) -> None: - repository = _make_active_repository(self.organization.id) - - with pytest.raises(SCMCodedError) as exc_info: - initialize_provider( - self.organization.id, - 1, - fetch_repository=lambda _, __: repository, - fetch_service_provider=lambda _, __: None, - ) - assert exc_info.value.code == "integration_not_found" - - -class TestExecProviderFn(TestCase): - def test_returns_provider_fn_result(self) -> None: - provider = _make_provider() - - result = exec_provider_fn( - provider, - provider_fn=lambda: "success", - ) - - assert result == "success" - - def test_raises_rate_limit_exceeded(self) -> None: - with pytest.raises(SCMCodedError) as exc_info: - exec_provider_fn( - _make_provider(is_rate_limited=True), - provider_fn=lambda: None, - ) - assert exc_info.value.code == "rate_limit_exceeded" - - def test_scm_provider_exception_is_reraised(self) -> None: - """SCMError subclasses from provider_fn should pass through unwrapped.""" - - def raise_scm_provider_exception(): - raise SCMProviderException("API failure") - - with pytest.raises(SCMProviderException, match="API failure"): - exec_provider_fn( - _make_provider(), - provider_fn=raise_scm_provider_exception, - ) - - def test_scm_coded_error_is_reraised(self) -> None: - """SCMCodedError from provider_fn should pass through unwrapped.""" - - def raise_scm_coded_error(): - raise SCMCodedError(code="unsupported_integration") - - with pytest.raises(SCMCodedError) as exc_info: - exec_provider_fn( - _make_provider(), - provider_fn=raise_scm_coded_error, - ) - assert exc_info.value.code == "unsupported_integration" - - def test_generic_exception_wrapped_in_scm_unhandled_exception(self) -> None: - """Non-SCMError exceptions should be wrapped in SCMUnhandledException.""" - - def raise_value_error(): - raise ValueError("something unexpected") - - with pytest.raises(SCMUnhandledException) as exc_info: - exec_provider_fn( - _make_provider(), - provider_fn=raise_value_error, - ) - assert isinstance(exc_info.value.__cause__, ValueError) - assert "something unexpected" in str(exc_info.value.__cause__) - - def test_key_error_wrapped_in_scm_unhandled_exception(self) -> None: - """KeyError (e.g. from malformed response) should be wrapped.""" - - def raise_key_error(): - raise KeyError("missing_field") - - with pytest.raises(SCMUnhandledException) as exc_info: - exec_provider_fn( - _make_provider(), - provider_fn=raise_key_error, - ) - assert isinstance(exc_info.value.__cause__, KeyError) - - def test_runtime_error_wrapped_in_scm_unhandled_exception(self) -> None: - """RuntimeError should be wrapped in SCMUnhandledException.""" - - def raise_runtime_error(): - raise RuntimeError("unexpected state") - - with pytest.raises(SCMUnhandledException) as exc_info: - exec_provider_fn( - _make_provider(), - provider_fn=raise_runtime_error, - ) - assert isinstance(exc_info.value.__cause__, RuntimeError) diff --git a/tests/sentry/scm/integration/test_rate_limit_integration.py b/tests/sentry/scm/integration/test_rate_limit_integration.py index 9ea1e3899bf367..fd96fa620d8f23 100644 --- a/tests/sentry/scm/integration/test_rate_limit_integration.py +++ b/tests/sentry/scm/integration/test_rate_limit_integration.py @@ -1,10 +1,7 @@ from django.conf import settings +from scm.rate_limit import total_limit_key, usage_count_key -from sentry.scm.private.rate_limit import ( - RedisRateLimitProvider, - total_limit_key, - usage_count_key, -) +from sentry.scm.private.rate_limit import RedisRateLimitProvider from sentry.testutils.cases import TestCase from sentry.utils import redis diff --git a/tests/sentry/scm/integration/test_scm_actions_integration.py b/tests/sentry/scm/integration/test_scm_actions_integration.py deleted file mode 100644 index 82ce1f3a43417d..00000000000000 --- a/tests/sentry/scm/integration/test_scm_actions_integration.py +++ /dev/null @@ -1,193 +0,0 @@ -from datetime import timedelta -from unittest import mock - -import pytest -from django.utils import timezone - -from sentry.constants import ObjectStatus -from sentry.scm.actions import SourceCodeManager -from sentry.scm.errors import SCMCodedError -from sentry.scm.private.providers.github import GitHubProvider -from sentry.testutils.cases import TestCase - - -class TestMakeFromRepositoryId(TestCase): - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def setUp(self, mock_get_jwt): - super().setUp() - ten_days = timezone.now() + timedelta(days=10) - self.integration = self.create_integration( - organization=self.organization, - provider="github", - name="Test GitHub", - external_id="12345", - metadata={ - "access_token": "12345token", - "expires_at": ten_days.strftime("%Y-%m-%dT%H:%M:%S"), - }, - ) - self.repo = self.create_repo( - name="test-org/test-repo", - provider="integrations:github", - integration_id=self.integration.id, - external_id="67890", - ) - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def test_success_with_integer_repository_id(self, mock_get_jwt): - scm = SourceCodeManager.make_from_repository_id( - self.organization.id, - self.repo.id, - ) - - assert isinstance(scm.provider, GitHubProvider) - assert scm.referrer == "shared" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def test_success_with_composite_repository_id(self, mock_get_jwt): - scm = SourceCodeManager.make_from_repository_id( - self.organization.id, - ("github", "67890"), - ) - - assert isinstance(scm.provider, GitHubProvider) - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def test_custom_referrer_is_stored(self, mock_get_jwt): - scm = SourceCodeManager.make_from_repository_id( - self.organization.id, - self.repo.id, - referrer="emerge", - ) - - assert scm.referrer == "emerge" - - def test_raises_repository_not_found_for_nonexistent_id(self) -> None: - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_repository_id( - self.organization.id, - 99999, - ) - - assert exc_info.value.code == "repository_not_found" - - def test_raises_repository_not_found_for_nonexistent_composite_id(self) -> None: - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_repository_id( - self.organization.id, - ("github", "nonexistent"), - ) - - assert exc_info.value.code == "repository_not_found" - - def test_raises_repository_inactive(self) -> None: - self.repo.status = ObjectStatus.DISABLED - self.repo.save() - - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_repository_id( - self.organization.id, - self.repo.id, - ) - - assert exc_info.value.code == "repository_inactive" - - def test_raises_repository_not_found_for_wrong_organization(self) -> None: - other_org = self.create_organization() - - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_repository_id( - other_org.id, - self.repo.id, - ) - - assert exc_info.value.code == "repository_not_found" - - def test_raises_integration_not_found_when_no_integration_exists(self) -> None: - repo = self.create_repo( - name="test-org/orphan-repo", - provider="integrations:github", - integration_id=99999, - external_id="11111", - ) - - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_repository_id( - self.organization.id, - repo.id, - ) - - assert exc_info.value.code == "integration_not_found" - - -class TestMakeFromIntegration(TestCase): - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def setUp(self, mock_get_jwt): - super().setUp() - ten_days = timezone.now() + timedelta(days=10) - self.integration = self.create_integration( - organization=self.organization, - provider="github", - name="Test GitHub", - external_id="12345", - metadata={ - "access_token": "12345token", - "expires_at": ten_days.strftime("%Y-%m-%dT%H:%M:%S"), - }, - ) - self.repo = self.create_repo( - name="test-org/test-repo", - provider="integrations:github", - integration_id=self.integration.id, - external_id="67890", - ) - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def test_success_with_github_integration(self, mock_get_jwt): - scm = SourceCodeManager.make_from_integration( - self.organization.id, - self.repo, - self.integration, - ) - - assert isinstance(scm.provider, GitHubProvider) - assert scm.referrer == "shared" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def test_custom_referrer_is_stored(self, mock_get_jwt): - scm = SourceCodeManager.make_from_integration( - self.organization.id, - self.repo, - self.integration, - referrer="emerge", - ) - - assert scm.referrer == "emerge" - - @mock.patch( - "sentry.scm.actions.map_integration_to_provider", - side_effect=SCMCodedError(code="unsupported_integration"), - ) - def test_raises_unsupported_integration_for_unknown_provider(self, mock_map): - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_integration( - self.organization.id, - self.repo, - self.integration, - ) - - assert exc_info.value.code == "unsupported_integration" - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - def test_raises_repository_inactive(self, mock_get_jwt): - self.repo.status = ObjectStatus.DISABLED - self.repo.save() - - with pytest.raises(SCMCodedError) as exc_info: - SourceCodeManager.make_from_integration( - self.organization.id, - self.repo, - self.integration, - ) - - assert exc_info.value.code == "repository_inactive" diff --git a/tests/sentry/scm/test_fixtures.py b/tests/sentry/scm/test_fixtures.py deleted file mode 100644 index 4054aec3ce9b0a..00000000000000 --- a/tests/sentry/scm/test_fixtures.py +++ /dev/null @@ -1,1569 +0,0 @@ -from datetime import datetime -from typing import Any -from unittest.mock import MagicMock - -from sentry.integrations.github.client import GitHubApiClient, GitHubReaction -from sentry.integrations.models import Integration -from sentry.scm.types import ( - ActionResult, - BuildConclusion, - BuildStatus, - CheckRun, - CheckRunOutput, - Comment, - Commit, - CommitAuthor, - CommitFile, - FileContent, - GitBlob, - GitCommitObject, - GitCommitTree, - GitRef, - GitTree, - InputTreeEntry, - PaginatedActionResult, - PaginatedResponseMeta, - PaginationParams, - Provider, - PullRequest, - PullRequestBranch, - PullRequestCommit, - PullRequestFile, - PullRequestState, - Reaction, - ReactionResult, - Referrer, - Repository, - RequestOptions, - Review, - ReviewComment, - ReviewCommentInput, - ReviewSide, - TreeEntry, -) -from sentry.shared_integrations.exceptions import ApiError - - -def make_github_comment( - comment_id: int = 1, - body: str = "Test comment", - user_id: int = 123, - username: str = "testuser", -) -> dict[str, Any]: - """Factory for GitHub comment API responses.""" - return { - "id": comment_id, - "body": body, - "user": {"id": user_id, "login": username}, - "created_at": "2026-02-04T10:00:00Z", - "updated_at": "2026-02-04T10:00:00Z", - } - - -def make_github_pull_request( - pr_id: int = 42, - number: int = 1, - title: str = "Test PR", - body: str | None = "PR description", - state: str = "open", - merged: bool = False, - url: str = "https://api.github.com/repos/test-org/test-repo/pulls/1", - html_url: str = "https://github.com/test-org/test-repo/pull/1", - head_sha: str = "abc123", - base_sha: str = "def456", - head_ref: str = "feature-branch", - base_ref: str = "main", - user_id: int = 123, - username: str = "testuser", -) -> dict[str, Any]: - """Factory for GitHub PR API responses.""" - return { - "id": pr_id, - "number": number, - "title": title, - "body": body, - "state": state, - "merged": merged, - "url": url, - "html_url": html_url, - "head": {"ref": head_ref, "sha": head_sha}, - "base": {"ref": base_ref, "sha": base_sha}, - "user": {"id": user_id, "login": username}, - } - - -def make_github_reaction( - reaction_id: int = 1, - content: str = "eyes", - user_id: int = 123, - username: str = "testuser", -) -> dict[str, Any]: - """Factory for GitHub reaction API responses.""" - return { - "id": reaction_id, - "content": content, - "user": {"id": user_id, "login": username}, - } - - -def make_github_branch( - branch: str = "main", - sha: str = "abc123def456", -) -> dict[str, Any]: - """Factory for GitHub branch API responses.""" - return { - "name": branch, - "commit": {"sha": sha}, - } - - -def make_github_git_ref( - ref: str = "refs/heads/main", - sha: str = "abc123def456", -) -> dict[str, Any]: - """Factory for GitHub git ref API responses.""" - return { - "ref": ref, - "object": {"sha": sha, "type": "commit"}, - } - - -def make_github_git_blob( - sha: str = "blob123abc", -) -> dict[str, Any]: - """Factory for GitHub git blob API responses.""" - return { - "sha": sha, - "url": f"https://api.github.com/repos/test-org/test-repo/git/blobs/{sha}", - } - - -def make_github_file_content( - path: str = "README.md", - sha: str = "abc123", - content: str = "SGVsbG8gV29ybGQ=", - encoding: str = "base64", - size: int = 11, -) -> dict[str, Any]: - """Factory for GitHub file content API responses.""" - return { - "path": path, - "sha": sha, - "content": content, - "encoding": encoding, - "size": size, - "type": "file", - } - - -def make_github_commit_file( - filename: str = "src/main.py", - status: str = "modified", - patch: str | None = "@@ -1,3 +1,4 @@\n+new line", -) -> dict[str, Any]: - """Factory for GitHub commit file entries.""" - result: dict[str, Any] = {"filename": filename, "status": status} - if patch is not None: - result["patch"] = patch - return result - - -def make_github_commit( - sha: str = "abc123", - message: str = "Fix bug", - author_name: str = "Test User", - author_email: str = "test@example.com", - author_date: str = "2026-02-04T10:00:00Z", - files: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Factory for GitHub commit API responses.""" - return { - "sha": sha, - "commit": { - "message": message, - "author": { - "name": author_name, - "email": author_email, - "date": author_date, - }, - }, - "files": files if files is not None else [make_github_commit_file()], - } - - -def make_github_commit_comparison( - ahead_by: int = 3, - behind_by: int = 1, - commits: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Factory for GitHub commit comparison API responses.""" - return { - "ahead_by": ahead_by, - "behind_by": behind_by, - "commits": commits if commits is not None else [], - } - - -def make_github_tree_entry( - path: str = "src/main.py", - mode: str = "100644", - entry_type: str = "blob", - sha: str = "abc123", - size: int | None = 1234, -) -> dict[str, Any]: - """Factory for GitHub tree entry objects.""" - result: dict[str, Any] = { - "path": path, - "mode": mode, - "type": entry_type, - "sha": sha, - } - if size is not None: - result["size"] = size - return result - - -def make_github_git_tree( - sha: str = "tree_sha_abc", - entries: list[dict[str, Any]] | None = None, - truncated: bool = False, -) -> dict[str, Any]: - """Factory for GitHub git tree API responses.""" - return { - "sha": sha, - "tree": entries if entries is not None else [make_github_tree_entry()], - "truncated": truncated, - } - - -def make_github_git_commit_object( - sha: str = "abc123", - tree_sha: str = "tree456", - message: str = "Initial commit", -) -> dict[str, Any]: - """Factory for GitHub git commit object API responses.""" - return { - "sha": sha, - "tree": {"sha": tree_sha}, - "message": message, - } - - -def make_github_pull_request_file( - filename: str = "src/main.py", - status: str = "modified", - patch: str | None = "@@ -1,3 +1,4 @@\n+new line", - changes: int = 1, - sha: str = "file123", - previous_filename: str | None = None, -) -> dict[str, Any]: - """Factory for GitHub pull request file API responses.""" - result: dict[str, Any] = { - "filename": filename, - "status": status, - "changes": changes, - "sha": sha, - } - if patch is not None: - result["patch"] = patch - if previous_filename is not None: - result["previous_filename"] = previous_filename - return result - - -def make_github_pull_request_commit( - sha: str = "commit123", - message: str = "Fix bug", - author_name: str = "Test User", - author_email: str = "test@example.com", - author_date: str = "2026-02-04T10:00:00Z", - author_login: str | None = "testuser", -) -> dict[str, Any]: - """Factory for GitHub pull request commit API responses.""" - result: dict[str, Any] = { - "sha": sha, - "commit": { - "message": message, - "author": { - "name": author_name, - "email": author_email, - "date": author_date, - }, - }, - } - if author_login is not None: - result["author"] = {"login": author_login} - else: - result["author"] = None - return result - - -def make_github_review_comment( - comment_id: int = 100, - html_url: str = "https://github.com/test-org/test-repo/pull/1#discussion_r100", - path: str = "src/main.py", - body: str = "Looks good", -) -> dict[str, Any]: - """Factory for GitHub review comment API responses.""" - return { - "id": comment_id, - "html_url": html_url, - "path": path, - "body": body, - } - - -def make_github_review( - review_id: int = 200, - html_url: str = "https://github.com/test-org/test-repo/pull/1#pullrequestreview-200", -) -> dict[str, Any]: - """Factory for GitHub review API responses.""" - return { - "id": review_id, - "html_url": html_url, - } - - -def make_github_check_run( - check_run_id: int = 300, - name: str = "Seer Review", - status: str = "completed", - conclusion: str | None = "success", - html_url: str = "https://github.com/test-org/test-repo/runs/300", -) -> dict[str, Any]: - """Factory for GitHub check run API responses.""" - return { - "id": check_run_id, - "name": name, - "status": status, - "conclusion": conclusion, - "html_url": html_url, - } - - -def make_github_graphql_issue_comment( - node_id: str = "IC_abc123", - body: str = "Test issue comment", - is_minimized: bool = False, - author_login: str = "testuser", - author_database_id: int = 123, - author_typename: str = "User", -) -> dict[str, Any]: - """Factory for GraphQL issue comment nodes.""" - return { - "id": node_id, - "body": body, - "isMinimized": is_minimized, - "author": { - "login": author_login, - "databaseId": author_database_id, - "__typename": author_typename, - }, - } - - -def make_github_graphql_review_thread_comment( - node_id: str = "PRRC_abc123", - full_database_id: int | None = 12345, - url: str = "https://github.com/test-org/test-repo/pull/1#discussion_r100", - body: str = "Review thread comment", - is_minimized: bool = False, - path: str | None = "src/main.py", - start_line: int | None = 1, - line: int | None = 5, - diff_hunk: str | None = "@@ -1,3 +1,4 @@", - created_at: str | None = "2026-02-04T10:00:00Z", - updated_at: str | None = "2026-02-04T10:00:00Z", - reactions: list[dict[str, Any]] | None = None, - reactions_total_count: int = 0, - author_login: str = "reviewer", - author_database_id: int = 456, - author_typename: str = "User", -) -> dict[str, Any]: - """Factory for GraphQL review thread comment nodes.""" - return { - "id": node_id, - "fullDatabaseId": full_database_id, - "url": url, - "body": body, - "isMinimized": is_minimized, - "path": path, - "startLine": start_line, - "line": line, - "diffHunk": diff_hunk, - "createdAt": created_at, - "updatedAt": updated_at, - "reactions": { - "nodes": reactions if reactions is not None else [], - "totalCount": reactions_total_count, - }, - "author": { - "login": author_login, - "databaseId": author_database_id, - "__typename": author_typename, - }, - } - - -def make_github_graphql_review_thread( - node_id: str = "PRT_abc123", - is_collapsed: bool = False, - is_outdated: bool = False, - is_resolved: bool = False, - comments: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - """Factory for GraphQL review thread nodes.""" - return { - "id": node_id, - "isCollapsed": is_collapsed, - "isOutdated": is_outdated, - "isResolved": is_resolved, - "comments": { - "nodes": ( - comments if comments is not None else [make_github_graphql_review_thread_comment()] - ), - }, - } - - -def make_github_graphql_pr_comments_response( - issue_comments: list[dict[str, Any]] | None = None, - review_threads: list[dict[str, Any]] | None = None, - comments_has_next_page: bool = False, - comments_end_cursor: str | None = None, - threads_has_next_page: bool = False, - threads_end_cursor: str | None = None, -) -> dict[str, Any]: - """Factory for a full GraphQL PR comments response (the 'data' dict).""" - return { - "repository": { - "pullRequest": { - "comments": { - "nodes": ( - issue_comments - if issue_comments is not None - else [make_github_graphql_issue_comment()] - ), - "pageInfo": { - "hasNextPage": comments_has_next_page, - "endCursor": comments_end_cursor, - }, - }, - "reviewThreads": { - "nodes": ( - review_threads - if review_threads is not None - else [make_github_graphql_review_thread()] - ), - "pageInfo": { - "hasNextPage": threads_has_next_page, - "endCursor": threads_end_cursor, - }, - }, - } - } - } - - -_DEFAULT_PAGINATED_META: PaginatedResponseMeta = PaginatedResponseMeta(next_cursor=None) - - -class BaseTestProvider(Provider): - organization_id: int - repository: Repository - - def is_rate_limited(self, referrer: Referrer) -> bool: - return False - - # Pull request - - def get_pull_request( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[PullRequest]: - raw = make_github_pull_request() - return ActionResult( - data=PullRequest( - id=str(raw["id"]), - number=raw["number"], - title=raw["title"], - body=raw["body"], - state=raw["state"], - merged=raw["merged"], - html_url=raw["html_url"], - head=PullRequestBranch(sha=raw["head"]["sha"], ref=raw["head"]["ref"]), - base=PullRequestBranch(sha=raw["base"]["sha"], ref=raw["base"]["ref"]), - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - # Issue comments - - def get_issue_comments( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: - return PaginatedActionResult( - data=[ - Comment( - id="101", - body="Test comment", - author={"id": "1", "username": "testuser"}, - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_issue_comment(self, issue_id: str, body: str) -> ActionResult[Comment]: - return ActionResult( - data=Comment(id="101", body=body, author=None), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def delete_issue_comment(self, issue_id: str, comment_id: str) -> None: - return None - - # Pull request comments - - def get_pull_request_comments( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: - return PaginatedActionResult( - data=[ - Comment( - id="201", - body="PR review comment", - author={"id": "2", "username": "reviewer"}, - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_pull_request_comment(self, pull_request_id: str, body: str) -> ActionResult[Comment]: - return ActionResult( - data=Comment(id="201", body=body, author=None), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def delete_pull_request_comment(self, pull_request_id: str, comment_id: str) -> None: - return None - - # Issue comment reactions - - def get_issue_comment_reactions( - self, - issue_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - return PaginatedActionResult( - data=[ - ReactionResult(id="1", content="+1", author={"id": "1", "username": "testuser"}), - ReactionResult(id="2", content="eyes", author={"id": "2", "username": "otheruser"}), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - return ActionResult( - data=ReactionResult(id="1", content=reaction, author=None), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def delete_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction_id: str - ) -> None: - return None - - # Pull request comment reactions - - def get_pull_request_comment_reactions( - self, - pull_request_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - return PaginatedActionResult( - data=[ - ReactionResult( - id="3", content="rocket", author={"id": "1", "username": "testuser"} - ), - ReactionResult( - id="4", content="hooray", author={"id": "2", "username": "otheruser"} - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - return ActionResult( - data=ReactionResult(id="1", content=reaction, author=None), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def delete_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction_id: str - ) -> None: - return None - - # Issue reactions - - def get_issue_reactions( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - return PaginatedActionResult( - data=[ - ReactionResult(id="1", content="+1", author={"id": "1", "username": "testuser"}), - ReactionResult( - id="2", content="heart", author={"id": "2", "username": "otheruser"} - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_issue_reaction( - self, issue_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - return ActionResult( - data=ReactionResult(id="1", content=reaction, author=None), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def delete_issue_reaction(self, issue_id: str, reaction_id: str) -> None: - return None - - # Pull request reactions - - def get_pull_request_reactions( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: - return PaginatedActionResult( - data=[ - ReactionResult(id="5", content="laugh", author={"id": "1", "username": "testuser"}), - ReactionResult( - id="6", content="confused", author={"id": "2", "username": "otheruser"} - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_pull_request_reaction( - self, pull_request_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: - return ActionResult( - data=ReactionResult(id="1", content=reaction, author=None), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def delete_pull_request_reaction(self, pull_request_id: str, reaction_id: str) -> None: - return None - - # Branch operations - - def get_branch( - self, - branch: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitRef]: - return ActionResult( - data=GitRef(ref=f"refs/heads/{branch}", sha="abc123def456"), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def create_branch(self, branch: str, sha: str) -> ActionResult[GitRef]: - return ActionResult( - data=GitRef(ref=branch, sha=sha), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def update_branch(self, branch: str, sha: str, force: bool = False) -> ActionResult[GitRef]: - return ActionResult( - data=GitRef(ref=branch, sha=sha), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - # Git blob operations - - def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: - return ActionResult( - data=GitBlob(sha="blob123abc"), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - # File content operations - - def get_file_content( - self, - path: str, - ref: str | None = None, - request_options: RequestOptions | None = None, - ) -> ActionResult[FileContent]: - return ActionResult( - data=FileContent( - path=path, - sha="abc123", - content="SGVsbG8gV29ybGQ=", - encoding="base64", - size=11, - ), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - # Commit operations - - def get_commit( - self, - sha: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[Commit]: - return ActionResult( - data=Commit( - id=sha, - message="Fix bug", - author=CommitAuthor( - name="Test User", - email="test@example.com", - date=datetime.fromisoformat("2026-02-04T10:00:00Z"), - ), - files=[CommitFile(filename="src/main.py", status="modified", patch="@@ -1 +1 @@")], - ), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def get_commits( - self, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - inner = self.get_commit("abc123") - return PaginatedActionResult( - data=[inner["data"]], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def get_commits_by_path( - self, - path: str, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - inner = self.get_commit("abc123") - return PaginatedActionResult( - data=[inner["data"]], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def compare_commits( - self, - start_sha: str, - end_sha: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: - inner = self.get_commit("abc123") - return PaginatedActionResult( - data=[inner["data"]], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - # Git data operations - - def get_tree( - self, - tree_sha: str, - recursive: bool = True, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitTree]: - return ActionResult( - data=GitTree( - sha=tree_sha, - tree=[ - TreeEntry( - path="src/main.py", mode="100644", type="blob", sha="abc123", size=1234 - ) - ], - truncated=False, - ), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def get_git_commit( - self, - sha: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitCommitObject]: - return ActionResult( - data=GitCommitObject( - sha=sha, - tree=GitCommitTree(sha="tree456"), - message="Initial commit", - ), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def create_git_tree( - self, - tree: list[InputTreeEntry], - base_tree: str | None = None, - ) -> ActionResult[GitTree]: - return ActionResult( - data=GitTree( - sha="newtree123", - tree=[ - TreeEntry( - path="src/main.py", mode="100644", type="blob", sha="new123", size=100 - ) - ], - truncated=False, - ), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def create_git_commit( - self, - message: str, - tree_sha: str, - parent_shas: list[str], - ) -> ActionResult[GitCommitObject]: - return ActionResult( - data=GitCommitObject( - sha="newcommit123", - tree=GitCommitTree(sha=tree_sha), - message=message, - ), - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - # Expanded pull request operations - - def get_pull_request_files( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestFile]: - return PaginatedActionResult( - data=[ - PullRequestFile( - filename="src/main.py", - status="modified", - patch="@@ -1 +1 @@", - changes=1, - sha="file123", - previous_filename=None, - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def get_pull_request_commits( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestCommit]: - return PaginatedActionResult( - data=[ - PullRequestCommit( - sha="commit123", - message="Fix bug", - author=CommitAuthor( - name="Test User", - email="test@example.com", - date=datetime.fromisoformat("2026-02-04T10:00:00Z"), - ), - ), - ], - type="github", - raw={"headers": None, "data": None}, - meta=_DEFAULT_PAGINATED_META, - ) - - def get_pull_request_diff( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[str]: - return ActionResult( - data="diff --git a/file.py b/file.py\n--- a/file.py\n+++ b/file.py\n@@ -1 +1 @@\n-old\n+new", - type="github", - raw={"headers": None, "data": None}, - meta={}, - ) - - def get_pull_requests( - self, - state: PullRequestState | None = "open", - head: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequest]: - raw = make_github_pull_request() - return PaginatedActionResult( - data=[ - PullRequest( - id=str(raw["id"]), - number=raw["number"], - title=raw["title"], - body=raw["body"], - state=raw["state"], - merged=raw["merged"], - html_url=raw["html_url"], - head=PullRequestBranch(sha=raw["head"]["sha"], ref=raw["head"]["ref"]), - base=PullRequestBranch(sha=raw["base"]["sha"], ref=raw["base"]["ref"]), - ), - ], - type="github", - raw={"headers": None, "data": raw}, - meta=_DEFAULT_PAGINATED_META, - ) - - def create_pull_request( - self, - title: str, - body: str, - head: str, - base: str, - ) -> ActionResult[PullRequest]: - raw = make_github_pull_request(title=title, body=body) - return ActionResult( - data=PullRequest( - id=str(raw["id"]), - number=raw["number"], - title=raw["title"], - body=raw["body"], - state=raw["state"], - merged=raw["merged"], - html_url=raw["html_url"], - head=PullRequestBranch(sha=raw["head"]["sha"], ref=raw["head"]["ref"]), - base=PullRequestBranch(sha=raw["base"]["sha"], ref=raw["base"]["ref"]), - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def create_pull_request_draft( - self, - title: str, - body: str, - head: str, - base: str, - ) -> ActionResult[PullRequest]: - raw = make_github_pull_request(title=title, body=body) - return ActionResult( - data=PullRequest( - id=str(raw["id"]), - number=raw["number"], - title=raw["title"], - body=raw["body"], - state=raw["state"], - merged=raw["merged"], - html_url=raw["html_url"], - head=PullRequestBranch(sha=raw["head"]["sha"], ref=raw["head"]["ref"]), - base=PullRequestBranch(sha=raw["base"]["sha"], ref=raw["base"]["ref"]), - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def update_pull_request( - self, - pull_request_id: str, - title: str | None = None, - body: str | None = None, - state: str | None = None, - ) -> ActionResult[PullRequest]: - raw = make_github_pull_request( - title=title or "Test PR", - body=body or "PR description", - state=state or "open", - ) - return ActionResult( - data=PullRequest( - id=str(raw["id"]), - number=raw["number"], - title=raw["title"], - body=raw["body"], - state=raw["state"], - merged=raw["merged"], - html_url=raw["html_url"], - head=PullRequestBranch(sha=raw["head"]["sha"], ref=raw["head"]["ref"]), - base=PullRequestBranch(sha=raw["base"]["sha"], ref=raw["base"]["ref"]), - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def request_review(self, pull_request_id: str, reviewers: list[str]) -> None: - return None - - # Review operations - - def create_review_comment_file( - self, - pull_request_id: str, - commit_id: str, - body: str, - path: str, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: - raw = make_github_review_comment(body=body, path=path) - return ActionResult( - data=ReviewComment( - id=str(raw["id"]), - html_url=raw["html_url"], - path=raw["path"], - body=raw["body"], - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def create_review_comment_reply( - self, - pull_request_id: str, - body: str, - comment_id: str, - ) -> ActionResult[ReviewComment]: - raw = make_github_review_comment(body=body) - return ActionResult( - data=ReviewComment( - id=str(raw["id"]), - html_url=raw["html_url"], - path=raw["path"], - body=raw["body"], - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def create_review( - self, - pull_request_id: str, - commit_sha: str, - event: str, - comments: list[ReviewCommentInput], - body: str | None = None, - ) -> ActionResult[Review]: - raw = make_github_review() - return ActionResult( - data=Review(id=str(raw["id"]), html_url=raw["html_url"]), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - # Check run operations - - def create_check_run( - self, - name: str, - head_sha: str, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - external_id: str | None = None, - started_at: str | None = None, - completed_at: str | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: - raw = make_github_check_run(name=name) - return ActionResult( - data=CheckRun( - id=str(raw["id"]), - name=raw["name"], - status=raw["status"], - conclusion=raw["conclusion"], - html_url=raw["html_url"], - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def get_check_run( - self, - check_run_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[CheckRun]: - raw = make_github_check_run() - return ActionResult( - data=CheckRun( - id=str(raw["id"]), - name=raw["name"], - status=raw["status"], - conclusion=raw["conclusion"], - html_url=raw["html_url"], - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - def update_check_run( - self, - check_run_id: str, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: - raw = make_github_check_run( - status=status or "completed", - conclusion=conclusion, - ) - return ActionResult( - data=CheckRun( - id=str(raw["id"]), - name=raw["name"], - status=raw["status"], - conclusion=raw["conclusion"], - html_url=raw["html_url"], - ), - type="github", - raw={"headers": None, "data": raw}, - meta={}, - ) - - # GraphQL mutation operations - - def minimize_comment(self, comment_node_id: str, reason: str) -> None: - return None - - -class FakeGitHubApiClient(GitHubApiClient): - """ - A fake GitHubApiClient for testing GitHubProvider without HTTP mocking. - - Configure responses by setting the corresponding attributes before calling - provider methods. Use `raise_api_error` to simulate API failures. - """ - - def __init__(self) -> None: - super().__init__(integration=MagicMock(spec=Integration)) - self.issue_comments: list[dict[str, Any]] = [] - self.pr_comments: list[dict[str, Any]] = [] - self.graphql_pr_comments_data: dict[str, Any] | None = None - self.minimize_comment_data: dict[str, Any] | None = None - self.resolve_thread_data: dict[str, Any] | None = None - self.delete_review_comment_data: dict[str, Any] | None = None - self.pull_request_data: dict[str, Any] | None = None - self.comment_reactions: list[dict[str, Any]] = [] - self.issue_reactions: list[dict[str, Any]] = [] - self.git_blob_data: dict[str, Any] | None = None - self.file_content_data: dict[str, Any] | None = None - self.commit_data: dict[str, Any] | None = None - self.commits_data: list[dict[str, Any]] | None = None - self.comparison_data: dict[str, Any] | None = None - self.tree_data: list[dict[str, Any]] | None = None - self.tree_full_data: dict[str, Any] | None = None - self.git_commit_data: dict[str, Any] | None = None - self.created_tree_data: dict[str, Any] | None = None - self.created_commit_data: dict[str, Any] | None = None - self.pr_files_data: list[dict[str, Any]] | None = None - self.pr_commits_data: list[dict[str, Any]] | None = None - self.pr_diff_data: str = "diff --git a/f.py b/f.py" - self.pull_requests_data: list[dict[str, Any]] | None = None - self.created_pr_data: dict[str, Any] | None = None - self.updated_pr_data: dict[str, Any] | None = None - self.review_comment_data: dict[str, Any] | None = None - self.review_data: dict[str, Any] | None = None - self.check_run_data: dict[str, Any] | None = None - self.updated_check_run_data: dict[str, Any] | None = None - self.archive_link_data: str = "https://codeload.github.com/test-org/test-repo/legacy.tar.gz/refs/heads/main?token=fake" - - self.raise_api_error: bool = False - self.calls: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = [] - - def _record_call(self, method: str, *args: Any, **kwargs: Any) -> None: - self.calls.append((method, args, kwargs)) - - def _maybe_raise(self) -> None: - if self.raise_api_error: - raise ApiError("Fake API error") - - def get_issue_comments(self, repo: str, issue_number: str) -> list[dict[str, Any]]: - self._record_call("get_issue_comments", repo, issue_number) - self._maybe_raise() - return self.issue_comments - - def get_pull_request(self, repo: str, pull_number: str) -> dict[str, Any]: - self._record_call("get_pull_request", repo, pull_number) - self._maybe_raise() - if self.pull_request_data is None: - return make_github_pull_request() - return self.pull_request_data - - def get_pull_request_comments(self, repo: str, pull_number: str) -> list[dict[str, Any]]: - self._record_call("get_pull_request_comments", repo, pull_number) - self._maybe_raise() - return self.pr_comments - - def get_pull_request_comments_graphql( - self, - owner: str, - repo: str, - pr_number: int, - *, - comments_after: str | None = None, - include_comments: bool = True, - review_threads_after: str | None = None, - include_threads: bool = True, - ) -> dict[str, Any]: - self._record_call( - "get_pull_request_comments_graphql", - owner, - repo, - pr_number, - comments_after=comments_after, - include_comments=include_comments, - review_threads_after=review_threads_after, - include_threads=include_threads, - ) - self._maybe_raise() - if self.graphql_pr_comments_data is not None: - return self.graphql_pr_comments_data - return make_github_graphql_pr_comments_response() - - def minimize_comment(self, comment_node_id: str, reason: str) -> dict[str, Any]: - self._record_call("minimize_comment", comment_node_id, reason) - self._maybe_raise() - if self.minimize_comment_data is not None: - return self.minimize_comment_data - return {"minimizeComment": {"minimizedComment": {"isMinimized": True}}} - - def delete_pull_request_review_comment(self, comment_node_id: str) -> dict[str, Any]: - self._record_call("delete_pull_request_review_comment", comment_node_id) - self._maybe_raise() - if self.delete_review_comment_data is not None: - return self.delete_review_comment_data - return {"deletePullRequestReviewComment": {"clientMutationId": None}} - - def create_comment(self, repo: str, issue_id: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_comment", repo, issue_id, data) - self._maybe_raise() - return make_github_comment(body=data.get("body", "")) - - def delete(self, path: str) -> None: - self._record_call("delete", path) - self._maybe_raise() - - def delete_issue_comment(self, repo: str, comment_id: str) -> None: - self._record_call("delete_issue_comment", repo, comment_id) - self._maybe_raise() - - def delete_comment_reaction(self, repo: str, comment_id: str, reaction_id: str) -> None: - self._record_call("delete_comment_reaction", repo, comment_id, reaction_id) - self._maybe_raise() - - def get_comment_reactions(self, repo: str, comment_id: str) -> list[dict[str, Any]]: - self._record_call("get_comment_reactions", repo, comment_id) - self._maybe_raise() - return self.comment_reactions - - def create_comment_reaction( - self, repo: str, comment_id: str, reaction: GitHubReaction - ) -> dict[str, Any]: - self._record_call("create_comment_reaction", repo, comment_id, reaction) - self._maybe_raise() - return make_github_reaction(content=reaction.value) - - def get_issue_reactions(self, repo: str, issue_number: str) -> list[dict[str, Any]]: - self._record_call("get_issue_reactions", repo, issue_number) - self._maybe_raise() - return self.issue_reactions - - def create_issue_reaction( - self, repo: str, issue_number: str, reaction: GitHubReaction - ) -> dict[str, Any]: - self._record_call("create_issue_reaction", repo, issue_number, reaction) - self._maybe_raise() - return make_github_reaction(content=reaction.value) - - def delete_issue_reaction(self, repo: str, issue_number: str, reaction_id: str) -> None: - self._record_call("delete_issue_reaction", repo, issue_number, reaction_id) - self._maybe_raise() - - def get_branch(self, repo: str, branch: str) -> dict[str, Any]: - self._record_call("get_branch", repo, branch) - self._maybe_raise() - return make_github_branch(branch=branch) - - def get_git_ref(self, repo: str, ref: str) -> dict[str, Any]: - self._record_call("get_git_ref", repo, ref) - self._maybe_raise() - return make_github_git_ref(ref=f"refs/heads/{ref}") - - def create_git_ref(self, repo: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_git_ref", repo, data) - self._maybe_raise() - return make_github_git_ref(ref=data.get("ref", ""), sha=data.get("sha", "")) - - def update_git_ref(self, repo: str, ref: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("update_git_ref", repo, ref, data) - self._maybe_raise() - return make_github_git_ref(ref=f"refs/heads/{ref}", sha=data.get("sha", "")) - - def create_git_blob(self, repo: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_git_blob", repo, data) - self._maybe_raise() - if self.git_blob_data is not None: - return self.git_blob_data - return make_github_git_blob() - - def get_file_content(self, repo: str, path: str, ref: str | None = None) -> dict[str, Any]: - self._record_call("get_file_content", repo, path, ref) - self._maybe_raise() - if self.file_content_data is not None: - return self.file_content_data - return make_github_file_content(path=path) - - def get_commit(self, repo: str, sha: str) -> dict[str, Any]: - self._record_call("get_commit", repo, sha) - self._maybe_raise() - if self.commit_data is not None: - return self.commit_data - return make_github_commit(sha=sha) - - def get_commits( - self, repo: str, sha: str | None = None, path: str | None = None - ) -> list[dict[str, Any]]: - self._record_call("get_commits", repo, sha=sha, path=path) - self._maybe_raise() - if self.commits_data is not None: - return self.commits_data - return [make_github_commit()] - - def compare_commits(self, repo: str, start_sha: str, end_sha: str) -> Any: - self._record_call("compare_commits", repo, start_sha, end_sha) - self._maybe_raise() - if self.comparison_data is not None: - return self.comparison_data - return [make_github_commit()] - - def get_tree(self, repo_full_name: str, tree_sha: str) -> list[dict[str, Any]]: - self._record_call("get_tree", repo_full_name, tree_sha) - self._maybe_raise() - if self.tree_data is not None: - return self.tree_data - return [make_github_tree_entry()] - - def get_tree_full( - self, repo_full_name: str, tree_sha: str, recursive: bool = True - ) -> dict[str, Any]: - self._record_call("get_tree_full", repo_full_name, tree_sha, recursive=recursive) - self._maybe_raise() - if self.tree_full_data is not None: - return self.tree_full_data - return make_github_git_tree() - - def get_git_commit(self, repo: str, sha: str) -> dict[str, Any]: - self._record_call("get_git_commit", repo, sha) - self._maybe_raise() - if self.git_commit_data is not None: - return self.git_commit_data - return make_github_git_commit_object(sha=sha) - - def create_git_tree(self, repo: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_git_tree", repo, data) - self._maybe_raise() - if self.created_tree_data is not None: - return self.created_tree_data - return make_github_git_tree() - - def create_git_commit(self, repo: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_git_commit", repo, data) - self._maybe_raise() - if self.created_commit_data is not None: - return self.created_commit_data - return make_github_git_commit_object( - sha="newcommit123", - tree_sha=data.get("tree", ""), - message=data.get("message", ""), - ) - - def get_pull_request_files(self, repo: str, pull_number: str) -> list[dict[str, Any]]: - self._record_call("get_pull_request_files", repo, pull_number) - self._maybe_raise() - if self.pr_files_data is not None: - return self.pr_files_data - return [make_github_pull_request_file()] - - def get_pull_request_commits(self, repo: str, pull_number: str) -> list[dict[str, Any]]: - self._record_call("get_pull_request_commits", repo, pull_number) - self._maybe_raise() - if self.pr_commits_data is not None: - return self.pr_commits_data - return [make_github_pull_request_commit()] - - def get_pull_request_diff(self, repo: str, pull_number: str) -> Any: - self._record_call("get_pull_request_diff", repo, pull_number) - self._maybe_raise() - return MagicMock(text=self.pr_diff_data) - - def list_pull_requests( - self, repo: str, state: str = "open", head: str | None = None - ) -> list[dict[str, Any]]: - self._record_call("list_pull_requests", repo, state, head) - self._maybe_raise() - if self.pull_requests_data is not None: - return self.pull_requests_data - return [make_github_pull_request()] - - def create_pull_request(self, repo: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_pull_request", repo, data) - self._maybe_raise() - if self.created_pr_data is not None: - return self.created_pr_data - return make_github_pull_request( - title=data.get("title", "Test PR"), - body=data.get("body", "PR description"), - ) - - def update_pull_request( - self, repo: str, pull_number: str, data: dict[str, Any] - ) -> dict[str, Any]: - self._record_call("update_pull_request", repo, pull_number, data) - self._maybe_raise() - if self.updated_pr_data is not None: - return self.updated_pr_data - return make_github_pull_request( - title=data.get("title", "Test PR"), - body=data.get("body", "PR description"), - state=data.get("state", "open"), - ) - - def create_review_request( - self, repo: str, pull_number: str, data: dict[str, Any] - ) -> dict[str, Any]: - self._record_call("create_review_request", repo, pull_number, data) - self._maybe_raise() - return {} - - def create_review_comment( - self, repo: str, pull_number: str, data: dict[str, Any] - ) -> dict[str, Any]: - self._record_call("create_review_comment", repo, pull_number, data) - self._maybe_raise() - if self.review_comment_data is not None: - return self.review_comment_data - return make_github_review_comment(body=data.get("body", "")) - - def create_review(self, repo: str, pull_number: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_review", repo, pull_number, data) - self._maybe_raise() - if self.review_data is not None: - return self.review_data - return make_github_review() - - def create_check_run(self, repo: str, data: dict[str, Any]) -> dict[str, Any]: - self._record_call("create_check_run", repo, data) - self._maybe_raise() - if self.check_run_data is not None: - return self.check_run_data - return make_github_check_run(name=data.get("name", "")) - - def get_check_run(self, repo: str, check_run_id: int) -> dict[str, Any]: - self._record_call("get_check_run", repo, check_run_id) - self._maybe_raise() - if self.check_run_data is not None: - return self.check_run_data - return make_github_check_run() - - def update_check_run( - self, repo: str, check_run_id: str, data: dict[str, Any] - ) -> dict[str, Any]: - self._record_call("update_check_run", repo, check_run_id, data) - self._maybe_raise() - if self.updated_check_run_data is not None: - return self.updated_check_run_data - return make_github_check_run( - status=data.get("status", "completed"), - conclusion=data.get("conclusion"), - ) - - def get_access_token(self, token_minimum_validity_time=None): - self._record_call("get_access_token") - return {"access_token": "fake-github-token", "permissions": None} - - def get_archive_link(self, repo: str, archive_format: str, ref: str) -> str: - self._record_call("get_archive_link", repo, archive_format, ref) - self._maybe_raise() - return self.archive_link_data diff --git a/tests/sentry/scm/unit/private/test_rate_limit.py b/tests/sentry/scm/unit/private/test_rate_limit.py deleted file mode 100644 index a6b50c2d78da95..00000000000000 --- a/tests/sentry/scm/unit/private/test_rate_limit.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import Callable - -import pytest - -from sentry.scm.private.rate_limit import ( - DynamicRateLimiter, - total_limit_key, - usage_count_key, -) - - -class MockRateLimitProvider: - def __init__(self, get_and_set_return: tuple[int | None, int], accounted_usage: int = 0): - self._get_and_set_return = get_and_set_return - self._accounted_usage = accounted_usage - self.accounted_keys: list[str] = [] - self.set_kvs: dict = {} - - def get_and_set_rate_limit(self, total_key, usage_key, expiration): - return self._get_and_set_return - - def get_accounted_usage(self, keys): - self.accounted_keys.extend(keys) - return self._accounted_usage - - def set_key_values(self, kvs): - self.set_kvs.update(kvs) - - -def make_limiter( - get_and_set_return: tuple[int | None, int] = (None, 0), - accounted_usage: int = 0, - referrer_allocation: dict | None = None, - recorded_capacity: int | None = None, - get_time_in_seconds: Callable[[], int] = lambda: 73, -) -> tuple[DynamicRateLimiter, MockRateLimitProvider]: - provider = MockRateLimitProvider(get_and_set_return, accounted_usage) - limiter = DynamicRateLimiter( - get_time_in_seconds=get_time_in_seconds, - organization_id=1, - provider="github", - rate_limit_provider=provider, - rate_limit_window_seconds=3600, - referrer_allocation=referrer_allocation or {}, - recorded_capacity=recorded_capacity, - ) - return limiter, provider - - -class TestIsRateLimited: - def test_allocated_referrer_with_excess_quota(self) -> None: - """Referrer with remaining quota is not rate limited.""" - limiter, _ = make_limiter( - get_and_set_return=(100, 10), - referrer_allocation={"my_referrer": 1.0}, - ) - assert limiter.is_rate_limited("my_referrer") is False - - def test_allocated_referrer_exhausted_quota(self) -> None: - """Referrer at quota limit is rate limited.""" - limiter, _ = make_limiter( - get_and_set_return=(10, 10), - referrer_allocation={"my_referrer": 1.0}, - ) - assert limiter.is_rate_limited("my_referrer") is True - - def test_shared_referrer_with_excess_quota(self) -> None: - """Shared referrer with remaining quota is not rate limited.""" - limiter, _ = make_limiter(get_and_set_return=(100, 10)) - assert limiter.is_rate_limited("shared") is False - - def test_shared_referrer_exhausted_quota(self) -> None: - """Shared referrer at quota limit is rate limited.""" - limiter, _ = make_limiter(get_and_set_return=(10, 10)) - assert limiter.is_rate_limited("shared") is True - - def test_unregistered_referrer_raises(self) -> None: - """A referrer not in the allocation pool must not be passed.""" - limiter, _ = make_limiter(get_and_set_return=(10, 10)) - with pytest.raises(AssertionError): - limiter.is_rate_limited("other") - - def test_fails_open_when_limit_not_set(self) -> None: - """Rate limit fails open if no limit is cached.""" - limiter, _ = make_limiter( - get_and_set_return=(None, 100_000_000), - referrer_allocation={"my_referrer": 0.000000001}, - ) - assert limiter.is_rate_limited("my_referrer") is False - - def test_caches_recorded_capacity_after_check(self) -> None: - """is_rate_limited stores the service capacity on the instance.""" - limiter, _ = make_limiter(get_and_set_return=(500, 1)) - limiter.is_rate_limited("shared") - assert limiter.recorded_capacity == 500 - - def test_fully_reserved_quota(self) -> None: - """Assert fully allocated referrer pool exhausts shared referrer by default.""" - limiter, _ = make_limiter( - get_and_set_return=(100, 10), - referrer_allocation={"my_referrer": 1.0}, - ) - assert limiter.is_rate_limited("shared") is True - - -class TestUpdateRateLimitMeta: - def test_updates_limit_and_shared_usage(self) -> None: - """Limit and shared usage are written when provider reports new values.""" - limiter, provider = make_limiter( - accounted_usage=40, - recorded_capacity=100, - referrer_allocation={"a": 0.05, "b": 0.01}, - ) - limiter.update_rate_limit_meta(capacity=110, consumed=50, next_window_start=3601) - - assert provider.accounted_keys == [ - usage_count_key("github", 1, 0, "a"), - usage_count_key("github", 1, 0, "b"), - ], provider.accounted_keys - assert provider.set_kvs == { - # Sentry said our limit was 100 but GitHub says its 110. GitHub wins. - total_limit_key("github", 1): (110, None), - # GitHub said 50 used but we recorded 40. Shared pool usage is set to reflect. - usage_count_key("github", 1, 0, "shared"): (10, 3527), - }, provider.set_kvs - - def test_accounted_keys_include_all_allocated_referrers(self) -> None: - """get_accounted_usage is called with all allocated referrer keys.""" - limiter, provider = make_limiter( - accounted_usage=40, - recorded_capacity=100, - ) - limiter.update_rate_limit_meta(capacity=110, consumed=50, next_window_start=3601) - - # No referrer allocation so not keys were looked up. - assert provider.accounted_keys == [] - - assert provider.set_kvs == { - # Sentry said our limit was 100 but GitHub says its 110. GitHub wins. - total_limit_key("github", 1): (110, None), - # GitHub said 50 used but we recorded 40. Shared pool usage is set to reflect. - usage_count_key("github", 1, 0, "shared"): (10, 3527), - }, provider.set_kvs - - def test_shared_usage_floored_at_zero(self) -> None: - """Shared usage is never negative when accounted exceeds reported.""" - limiter, provider = make_limiter(accounted_usage=100, recorded_capacity=100) - limiter.update_rate_limit_meta(capacity=110, consumed=50, next_window_start=3601) - - assert provider.set_kvs[usage_count_key("github", 1, 0, "shared")] == (0, 3527) - - def test_window_miss_skips_shared_usage_update(self) -> None: - """Shared usage is not written when provider window does not match.""" - limiter, provider = make_limiter(recorded_capacity=100) - limiter.update_rate_limit_meta(capacity=110, consumed=50, next_window_start=0) - - # Only the new capacity value was written. - assert provider.set_kvs == {total_limit_key("github", 1): (110, None)} - - def test_matching_limits_skips_total_key_write(self) -> None: - """Capacity key is not written when recorded and specified capacities match.""" - limiter, provider = make_limiter(recorded_capacity=110, accounted_usage=0) - limiter.update_rate_limit_meta(capacity=110, consumed=50, next_window_start=3601) - - # Service limit not overwritten. - assert provider.set_kvs == {usage_count_key("github", 1, 0, "shared"): (50, 3527)} - - def test_matching_limits_and_window_miss_writes_nothing(self) -> None: - """No writes when capacities match and windows differ.""" - limiter, provider = make_limiter(accounted_usage=50, recorded_capacity=110) - limiter.update_rate_limit_meta(capacity=110, consumed=50, next_window_start=0) - - # No values written. - assert provider.set_kvs == {} diff --git a/tests/sentry/scm/unit/test_github_provider.py b/tests/sentry/scm/unit/test_github_provider.py deleted file mode 100644 index 2f18c974c8e34e..00000000000000 --- a/tests/sentry/scm/unit/test_github_provider.py +++ /dev/null @@ -1,1110 +0,0 @@ -from datetime import datetime -from typing import Any -from unittest.mock import MagicMock - -import pytest - -from sentry.integrations.github.client import GitHubApiClient -from sentry.scm.errors import SCMProviderException -from sentry.scm.private.providers.github import ( - MINIMIZE_COMMENT_MUTATION, - GitHubProvider, - GitHubProviderApiClient, -) -from sentry.scm.types import Referrer, Repository -from tests.sentry.scm.test_fixtures import ( - make_github_branch, - make_github_check_run, - make_github_comment, - make_github_commit, - make_github_commit_comparison, - make_github_file_content, - make_github_git_blob, - make_github_git_commit_object, - make_github_git_ref, - make_github_git_tree, - make_github_pull_request, - make_github_pull_request_commit, - make_github_pull_request_file, - make_github_reaction, - make_github_review, - make_github_review_comment, -) - - -def make_repository() -> Repository: - return { - "integration_id": 1, - "name": "test-org/test-repo", - "organization_id": 1, - "is_active": True, - "external_id": None, - } - - -class FakeResponse: - def __init__( - self, - payload: Any, - *, - headers: dict[str, str] | None = None, - status_code: int | None = None, - text: str | None = None, - url: str = "", - ) -> None: - self._payload = payload - self.headers = headers or {} - self.status_code = status_code - self.text = text if text is not None else "" - self.url = url - - def json(self) -> Any: - return self._payload - - -class RecordingClient: - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - self.responses: dict[str, list[Any]] = { - "get": [], - "post": [], - "patch": [], - "delete": [], - "request": [], - "graphql": [], - } - - def queue(self, operation: str, response: Any) -> None: - self.responses[operation].append(response) - - def _pop(self, operation: str) -> Any: - if not self.responses[operation]: - raise AssertionError(f"No queued response for {operation}") - return self.responses[operation].pop(0) - - def is_rate_limited(self, referrer: Referrer) -> bool: - return False - - def get( - self, - path: str, - params: dict[str, Any] | None = None, - pagination: Any | None = None, - request_options: Any | None = None, - extra_headers: dict[str, str] | None = None, - allow_redirects: bool | None = None, - ) -> FakeResponse: - self.calls.append( - { - "operation": "get", - "path": path, - "params": params, - "pagination": pagination, - "request_options": request_options, - "extra_headers": extra_headers, - } - ) - return self._pop("get") - - def post( - self, - path: str, - data: dict[str, Any], - headers: dict[str, str] | None = None, - ) -> FakeResponse: - self.calls.append({"operation": "post", "path": path, "data": data, "headers": headers}) - return self._pop("post") - - def patch( - self, - path: str, - data: dict[str, Any], - headers: dict[str, str] | None = None, - ) -> FakeResponse: - self.calls.append({"operation": "patch", "path": path, "data": data, "headers": headers}) - return self._pop("patch") - - def delete(self, path: str) -> FakeResponse: - self.calls.append({"operation": "delete", "path": path}) - return self._pop("delete") - - def request( - self, - method: str, - path: str, - data: dict[str, Any] | None = None, - params: dict[str, str] | None = None, - headers: dict[str, str] | None = None, - ) -> FakeResponse: - self.calls.append( - { - "operation": "request", - "method": method, - "path": path, - "data": data, - "params": params, - "headers": headers, - } - ) - return self._pop("request") - - def graphql(self, query: str, variables: dict[str, Any]) -> dict[str, Any]: - self.calls.append({"operation": "graphql", "query": query, "variables": variables}) - return self._pop("graphql") - - -class NoOpRateLimitProvider: - def get_and_set_rate_limit( - self, total_key: str, usage_key: str, expiration: int - ) -> tuple[int | None, int]: - return (None, 0) - - def get_accounted_usage(self, keys: list[str]) -> int: - return 0 - - def set_key_values(self, kvs: dict[str, tuple[int, int | None]]) -> None: - pass - - -def make_provider(client: RecordingClient | None = None) -> tuple[GitHubProvider, RecordingClient]: - transport = client or RecordingClient() - provider = GitHubProvider( - MagicMock(spec=GitHubApiClient), - organization_id=1, - repository=make_repository(), - rate_limit_provider=NoOpRateLimitProvider(), - ) - provider.client = transport # type: ignore[assignment] - return provider, transport - - -def expected_comment(raw: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(raw["id"]), - "body": raw["body"], - "author": {"id": str(raw["user"]["id"]), "username": raw["user"]["login"]}, - } - - -def expected_reaction(raw: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(raw["id"]), - "content": raw["content"], - "author": {"id": str(raw["user"]["id"]), "username": raw["user"]["login"]}, - } - - -def expected_pull_request(raw: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(raw["id"]), - "number": str(raw["number"]), - "title": raw["title"], - "body": raw.get("body"), - "state": raw["state"], - "merged": raw.get("merged_at") is not None, - "html_url": raw.get("html_url", ""), - "head": {"sha": raw["head"]["sha"], "ref": raw["head"]["ref"]}, - "base": {"sha": raw["base"]["sha"], "ref": raw["base"]["ref"]}, - } - - -def expected_git_ref_from_branch(raw: dict[str, Any]) -> dict[str, Any]: - return {"ref": raw["name"], "sha": raw["commit"]["sha"]} - - -def expected_git_ref(raw: dict[str, Any]) -> dict[str, Any]: - return {"ref": raw["ref"].removeprefix("refs/heads/"), "sha": raw["object"]["sha"]} - - -def expected_file_content(raw: dict[str, Any]) -> dict[str, Any]: - return { - "path": raw["path"], - "sha": raw["sha"], - "content": raw.get("content", ""), - "encoding": raw.get("encoding", ""), - "size": raw["size"], - } - - -def expected_commit(raw: dict[str, Any]) -> dict[str, Any]: - author = raw["commit"]["author"] - return { - "id": raw["sha"], - "message": raw["commit"]["message"], - "author": { - "name": author["name"], - "email": author["email"], - "date": datetime.fromisoformat(author["date"]), - }, - "files": [ - { - "filename": entry["filename"], - "status": entry.get("status", "modified"), - "patch": entry.get("patch"), - } - for entry in raw.get("files", []) - ], - } - - -def expected_tree(raw: dict[str, Any]) -> dict[str, Any]: - return { - "sha": raw["sha"], - "tree": [ - { - "path": entry["path"], - "mode": entry["mode"], - "type": entry["type"], - "sha": entry["sha"], - "size": entry.get("size"), - } - for entry in raw["tree"] - ], - "truncated": raw["truncated"], - } - - -def expected_git_commit_object(raw: dict[str, Any]) -> dict[str, Any]: - return { - "sha": raw["sha"], - "tree": {"sha": raw["tree"]["sha"]}, - "message": raw.get("message", ""), - } - - -def expected_pull_request_file(raw: dict[str, Any]) -> dict[str, Any]: - return { - "filename": raw["filename"], - "status": raw.get("status", "modified"), - "patch": raw.get("patch"), - "changes": raw.get("changes", 0), - "sha": raw.get("sha", ""), - "previous_filename": raw.get("previous_filename"), - } - - -def expected_pull_request_commit(raw: dict[str, Any]) -> dict[str, Any]: - author = raw["commit"]["author"] - return { - "sha": raw["sha"], - "message": raw["commit"]["message"], - "author": { - "name": author["name"], - "email": author["email"], - "date": datetime.fromisoformat(author["date"]), - }, - } - - -def expected_review_comment(raw: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(raw["id"]), - "html_url": raw["html_url"], - "path": raw["path"], - "body": raw["body"], - } - - -def expected_review(raw: dict[str, Any]) -> dict[str, Any]: - return {"id": str(raw["id"]), "html_url": raw["html_url"]} - - -def expected_check_run(raw: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(raw["id"]), - "name": raw["name"], - "status": "completed" if raw["status"] == "completed" else "pending", - "conclusion": raw["conclusion"], - "html_url": raw["html_url"], - } - - -COMMENT_RAW = make_github_comment() -REACTION_RAW = make_github_reaction() -PULL_REQUEST_RAW = make_github_pull_request() -BRANCH_RAW = make_github_branch() -GIT_REF_RAW = make_github_git_ref() -GIT_BLOB_RAW = make_github_git_blob() -FILE_CONTENT_RAW = make_github_file_content() -COMMIT_RAW = make_github_commit() -COMPARISON_RAW = make_github_commit_comparison(commits=[COMMIT_RAW]) -TREE_RAW = make_github_git_tree() -GIT_COMMIT_OBJECT_RAW = make_github_git_commit_object() -PULL_REQUEST_FILE_RAW = make_github_pull_request_file(previous_filename="src/old.py") -PULL_REQUEST_COMMIT_RAW = make_github_pull_request_commit() -REVIEW_COMMENT_RAW = make_github_review_comment() -REVIEW_RAW = make_github_review() -CHECK_RUN_RAW = make_github_check_run() - - -PAGINATED_CASES: list[dict[str, Any]] = [ - { - "name": "get_issue_comments", - "kwargs": {"issue_id": "42"}, - "path": "/repos/test-org/test-repo/issues/42/comments", - "params": None, - "pagination": None, - "raw": [COMMENT_RAW], - "expected_data": [expected_comment(COMMENT_RAW)], - "next_cursor": "2", - }, - { - "name": "get_pull_request_comments", - "kwargs": {"pull_request_id": "42", "pagination": {"cursor": "4", "per_page": 25}}, - "path": "/repos/test-org/test-repo/issues/42/comments", - "params": None, - "pagination": {"cursor": "4", "per_page": 25}, - "raw": [COMMENT_RAW], - "expected_data": [expected_comment(COMMENT_RAW)], - "next_cursor": "5", - }, - { - "name": "get_issue_comment_reactions", - "kwargs": {"issue_id": "42", "comment_id": "99"}, - "path": "/repos/test-org/test-repo/issues/comments/99/reactions", - "params": None, - "pagination": None, - "raw": [REACTION_RAW], - "expected_data": [expected_reaction(REACTION_RAW)], - "next_cursor": "2", - }, - { - "name": "get_issue_reactions", - "kwargs": {"issue_id": "42"}, - "path": "/repos/test-org/test-repo/issues/42/reactions", - "params": None, - "pagination": None, - "raw": [REACTION_RAW], - "expected_data": [expected_reaction(REACTION_RAW)], - "next_cursor": "2", - }, - { - "name": "get_commits", - "kwargs": {"ref": "main", "pagination": {"cursor": "3", "per_page": 10}}, - "path": "/repos/test-org/test-repo/commits", - "params": {"sha": "main"}, - "pagination": {"cursor": "3", "per_page": 10}, - "raw": [COMMIT_RAW], - "expected_data": [expected_commit(COMMIT_RAW)], - "next_cursor": "4", - }, - { - "name": "get_commits_by_path", - "kwargs": {"path": "src/main.py", "ref": "main"}, - "path": "/repos/test-org/test-repo/commits", - "params": {"path": "src/main.py", "sha": "main"}, - "pagination": None, - "raw": [COMMIT_RAW], - "expected_data": [expected_commit(COMMIT_RAW)], - "next_cursor": "2", - }, - { - "name": "compare_commits", - "kwargs": {"start_sha": "aaa", "end_sha": "bbb"}, - "path": "/repos/test-org/test-repo/compare/aaa...bbb", - "params": None, - "pagination": None, - "raw": COMPARISON_RAW, - "expected_data": [expected_commit(COMMIT_RAW)], - "next_cursor": "2", - }, - { - "name": "get_pull_request_files", - "kwargs": {"pull_request_id": "42"}, - "path": "/repos/test-org/test-repo/pulls/42/files", - "params": None, - "pagination": None, - "raw": [PULL_REQUEST_FILE_RAW], - "expected_data": [expected_pull_request_file(PULL_REQUEST_FILE_RAW)], - "next_cursor": "2", - }, - { - "name": "get_pull_request_commits", - "kwargs": {"pull_request_id": "42"}, - "path": "/repos/test-org/test-repo/pulls/42/commits", - "params": None, - "pagination": None, - "raw": [PULL_REQUEST_COMMIT_RAW], - "expected_data": [expected_pull_request_commit(PULL_REQUEST_COMMIT_RAW)], - "next_cursor": "2", - }, - { - "name": "get_pull_requests", - "kwargs": { - "state": None, - "head": "octocat:feature", - "pagination": {"cursor": "2", "per_page": 15}, - }, - "path": "/repos/test-org/test-repo/pulls", - "params": {"state": "all", "head": "octocat:feature"}, - "pagination": {"cursor": "2", "per_page": 15}, - "raw": [PULL_REQUEST_RAW], - "expected_data": [expected_pull_request(PULL_REQUEST_RAW)], - "next_cursor": "3", - }, -] - - -ACTION_CASES: list[dict[str, Any]] = [ - { - "name": "create_issue_comment", - "operation": "post", - "kwargs": {"issue_id": "42", "body": "hello"}, - "path": "/repos/test-org/test-repo/issues/42/comments", - "data": {"body": "hello"}, - "raw": COMMENT_RAW, - "expected_data": expected_comment(COMMENT_RAW), - }, - { - "name": "get_pull_request", - "operation": "get", - "kwargs": {"pull_request_id": "42"}, - "path": "/repos/test-org/test-repo/pulls/42", - "raw": PULL_REQUEST_RAW, - "expected_data": expected_pull_request(PULL_REQUEST_RAW), - }, - { - "name": "create_pull_request_comment", - "operation": "post", - "kwargs": {"pull_request_id": "42", "body": "hello"}, - "path": "/repos/test-org/test-repo/issues/42/comments", - "data": {"body": "hello"}, - "raw": COMMENT_RAW, - "expected_data": expected_comment(COMMENT_RAW), - }, - { - "name": "create_issue_comment_reaction", - "operation": "post", - "kwargs": {"issue_id": "42", "comment_id": "99", "reaction": "heart"}, - "path": "/repos/test-org/test-repo/issues/comments/99/reactions", - "data": {"content": "heart"}, - "raw": REACTION_RAW, - "expected_data": expected_reaction(REACTION_RAW), - }, - { - "name": "create_issue_reaction", - "operation": "post", - "kwargs": {"issue_id": "42", "reaction": "rocket"}, - "path": "/repos/test-org/test-repo/issues/42/reactions", - "data": {"content": "rocket"}, - "raw": REACTION_RAW, - "expected_data": expected_reaction(REACTION_RAW), - }, - { - "name": "get_branch", - "operation": "get", - "kwargs": {"branch": "main"}, - "path": "/repos/test-org/test-repo/branches/main", - "raw": BRANCH_RAW, - "expected_data": expected_git_ref_from_branch(BRANCH_RAW), - }, - { - "name": "create_branch", - "operation": "post", - "kwargs": {"branch": "feature", "sha": "abc123"}, - "path": "/repos/test-org/test-repo/git/refs", - "data": {"ref": "refs/heads/feature", "sha": "abc123"}, - "raw": GIT_REF_RAW, - "expected_data": expected_git_ref(GIT_REF_RAW), - }, - { - "name": "update_branch", - "operation": "patch", - "kwargs": {"branch": "feature", "sha": "abc123", "force": True}, - "path": "/repos/test-org/test-repo/git/refs/heads/feature", - "data": {"sha": "abc123", "force": True}, - "raw": GIT_REF_RAW, - "expected_data": expected_git_ref(GIT_REF_RAW), - }, - { - "name": "create_git_blob", - "operation": "post", - "kwargs": {"content": "hello", "encoding": "utf-8"}, - "path": "/repos/test-org/test-repo/git/blobs", - "data": {"content": "hello", "encoding": "utf-8"}, - "raw": GIT_BLOB_RAW, - "expected_data": {"sha": GIT_BLOB_RAW["sha"]}, - }, - { - "name": "get_file_content", - "operation": "get", - "kwargs": {"path": "README.md", "ref": "main"}, - "path": "/repos/test-org/test-repo/contents/README.md", - "params": {"ref": "main"}, - "raw": FILE_CONTENT_RAW, - "expected_data": expected_file_content(FILE_CONTENT_RAW), - }, - { - "name": "get_commit", - "operation": "get", - "kwargs": {"sha": "abc123"}, - "path": "/repos/test-org/test-repo/commits/abc123", - "raw": COMMIT_RAW, - "expected_data": expected_commit(COMMIT_RAW), - }, - { - "name": "get_tree", - "operation": "get", - "kwargs": {"tree_sha": "tree123", "recursive": False}, - "path": "/repos/test-org/test-repo/git/trees/tree123", - "params": {}, - "raw": TREE_RAW, - "expected_data": expected_tree(TREE_RAW), - }, - { - "name": "get_git_commit", - "operation": "get", - "kwargs": {"sha": "abc123"}, - "path": "/repos/test-org/test-repo/git/commits/abc123", - "raw": GIT_COMMIT_OBJECT_RAW, - "expected_data": expected_git_commit_object(GIT_COMMIT_OBJECT_RAW), - }, - { - "name": "create_git_tree", - "operation": "post", - "kwargs": { - "tree": [{"path": "f.py", "mode": "100644", "type": "blob", "sha": "abc"}], - "base_tree": "base123", - }, - "path": "/repos/test-org/test-repo/git/trees", - "data": { - "tree": [{"path": "f.py", "mode": "100644", "type": "blob", "sha": "abc"}], - "base_tree": "base123", - }, - "raw": TREE_RAW, - "expected_data": expected_tree(TREE_RAW), - }, - { - "name": "create_git_commit", - "operation": "post", - "kwargs": {"message": "msg", "tree_sha": "tree123", "parent_shas": ["p1", "p2"]}, - "path": "/repos/test-org/test-repo/git/commits", - "data": {"message": "msg", "tree": "tree123", "parents": ["p1", "p2"]}, - "raw": GIT_COMMIT_OBJECT_RAW, - "expected_data": expected_git_commit_object(GIT_COMMIT_OBJECT_RAW), - }, - { - "name": "create_pull_request", - "operation": "post", - "kwargs": {"title": "T", "body": "B", "head": "feature", "base": "main"}, - "path": "/repos/test-org/test-repo/pulls", - "data": {"title": "T", "body": "B", "head": "feature", "base": "main"}, - "raw": PULL_REQUEST_RAW, - "expected_data": expected_pull_request(PULL_REQUEST_RAW), - }, - { - "name": "create_pull_request_draft", - "operation": "post", - "kwargs": {"title": "T", "body": "B", "head": "feature", "base": "main"}, - "path": "/repos/test-org/test-repo/pulls", - "data": {"title": "T", "body": "B", "head": "feature", "base": "main", "draft": True}, - "raw": PULL_REQUEST_RAW, - "expected_data": expected_pull_request(PULL_REQUEST_RAW), - }, - { - "name": "update_pull_request", - "operation": "patch", - "kwargs": {"pull_request_id": "42", "title": "New", "body": "Body", "state": "closed"}, - "path": "/repos/test-org/test-repo/pulls/42", - "data": {"title": "New", "body": "Body", "state": "closed"}, - "raw": PULL_REQUEST_RAW, - "expected_data": expected_pull_request(PULL_REQUEST_RAW), - }, - { - "name": "create_review_comment_file", - "operation": "post", - "kwargs": { - "pull_request_id": "42", - "commit_id": "abc123", - "body": "Looks good", - "path": "src/main.py", - "side": "RIGHT", - }, - "path": "/repos/test-org/test-repo/pulls/42/comments", - "data": { - "body": "Looks good", - "commit_id": "abc123", - "path": "src/main.py", - "side": "RIGHT", - "subject_type": "file", - }, - "raw": REVIEW_COMMENT_RAW, - "expected_data": expected_review_comment(REVIEW_COMMENT_RAW), - }, - { - "name": "create_review_comment_reply", - "operation": "post", - "kwargs": {"pull_request_id": "42", "body": "reply", "comment_id": "99"}, - "path": "/repos/test-org/test-repo/pulls/42/comments", - "data": {"body": "reply", "in_reply_to": 99}, - "raw": REVIEW_COMMENT_RAW, - "expected_data": expected_review_comment(REVIEW_COMMENT_RAW), - }, - { - "name": "create_review", - "operation": "post", - "kwargs": { - "pull_request_id": "42", - "commit_sha": "abc123", - "event": "approve", - "comments": [{"path": "f.py", "body": "fix"}], - "body": "overall", - }, - "path": "/repos/test-org/test-repo/pulls/42/reviews", - "data": { - "commit_id": "abc123", - "event": "APPROVE", - "comments": [{"path": "f.py", "body": "fix"}], - "body": "overall", - }, - "raw": REVIEW_RAW, - "expected_data": expected_review(REVIEW_RAW), - }, - { - "name": "create_check_run", - "operation": "post", - "kwargs": { - "name": "Seer Review", - "head_sha": "abc123", - "status": "running", - "conclusion": "success", - "external_id": "ext-1", - "started_at": "2026-02-04T10:00:00Z", - "completed_at": "2026-02-04T10:05:00Z", - "output": {"title": "Review", "summary": "All good"}, - }, - "path": "/repos/test-org/test-repo/check-runs", - "data": { - "name": "Seer Review", - "head_sha": "abc123", - "status": "in_progress", - "conclusion": "success", - "external_id": "ext-1", - "started_at": "2026-02-04T10:00:00Z", - "completed_at": "2026-02-04T10:05:00Z", - "output": {"title": "Review", "summary": "All good"}, - }, - "raw": CHECK_RUN_RAW, - "expected_data": expected_check_run(CHECK_RUN_RAW), - }, - { - "name": "get_check_run", - "operation": "get", - "kwargs": {"check_run_id": "300"}, - "path": "/repos/test-org/test-repo/check-runs/300", - "raw": CHECK_RUN_RAW, - "expected_data": expected_check_run(CHECK_RUN_RAW), - }, - { - "name": "update_check_run", - "operation": "patch", - "kwargs": { - "check_run_id": "300", - "status": "completed", - "conclusion": "failure", - "output": {"title": "Done", "summary": "Failed"}, - }, - "path": "/repos/test-org/test-repo/check-runs/300", - "data": { - "status": "completed", - "conclusion": "failure", - "output": {"title": "Done", "summary": "Failed"}, - }, - "raw": CHECK_RUN_RAW, - "expected_data": expected_check_run(CHECK_RUN_RAW), - }, - { - "name": "get_archive_link", - "id": "get_archive_link_tarball", - "operation": "get", - "status_code": 302, - "kwargs": {"ref": "main"}, - "path": "/repos/test-org/test-repo/tarball/main", - "headers": { - "Location": "https://codeload.github.com/test-org/test-repo/legacy.tar.gz/refs/heads/main" - }, - "raw": "https://codeload.github.com/test-org/test-repo/legacy.tar.gz/refs/heads/main", - "expected_data": { - "url": "https://codeload.github.com/test-org/test-repo/legacy.tar.gz/refs/heads/main", - "headers": {}, - }, - }, - { - "name": "get_archive_link", - "id": "get_archive_link_zip", - "operation": "get", - "status_code": 302, - "kwargs": {"ref": "main", "archive_format": "zip"}, - "path": "/repos/test-org/test-repo/zipball/main", - "headers": { - "Location": "https://codeload.github.com/test-org/test-repo/legacy.zip/refs/heads/main" - }, - "raw": "https://codeload.github.com/test-org/test-repo/legacy.zip/refs/heads/main", - "expected_data": { - "url": "https://codeload.github.com/test-org/test-repo/legacy.zip/refs/heads/main", - "headers": {}, - }, - }, -] - - -VOID_CASES: list[dict[str, Any]] = [ - { - "name": "delete_issue_comment", - "operation": "delete", - "kwargs": {"issue_id": "42", "comment_id": "99"}, - "path": "/repos/test-org/test-repo/issues/comments/99", - }, - { - "name": "delete_pull_request_comment", - "operation": "delete", - "kwargs": {"pull_request_id": "42", "comment_id": "99"}, - "path": "/repos/test-org/test-repo/issues/comments/99", - }, - { - "name": "delete_issue_comment_reaction", - "operation": "delete", - "kwargs": {"issue_id": "42", "comment_id": "99", "reaction_id": "5"}, - "path": "/repos/test-org/test-repo/issues/comments/99/reactions/5", - }, - { - "name": "delete_issue_reaction", - "operation": "delete", - "kwargs": {"issue_id": "42", "reaction_id": "5"}, - "path": "/repos/test-org/test-repo/issues/42/reactions/5", - }, - { - "name": "request_review", - "operation": "post", - "kwargs": {"pull_request_id": "42", "reviewers": ["octocat"]}, - "path": "/repos/test-org/test-repo/pulls/42/requested_reviewers", - "data": {"reviewers": ["octocat"]}, - }, - { - "name": "minimize_comment", - "operation": "graphql", - "kwargs": {"comment_node_id": "IC_123", "reason": "OUTDATED"}, - "query": MINIMIZE_COMMENT_MUTATION, - "variables": {"commentId": "IC_123", "reason": "OUTDATED"}, - }, -] - - -ALIAS_METHODS = { - "get_pull_request_comment_reactions": ( - "get_issue_comment_reactions", - {"pull_request_id": "42", "comment_id": "99", "pagination": {"cursor": "2", "per_page": 5}}, - ("42", "99", {"cursor": "2", "per_page": 5}, None), - {"data": ["ok"], "type": "github", "raw": [], "meta": {"next_cursor": "3"}}, - ), - "create_pull_request_comment_reaction": ( - "create_issue_comment_reaction", - {"pull_request_id": "42", "comment_id": "99", "reaction": "heart"}, - ("42", "99", "heart"), - {"data": {"id": "1"}, "type": "github", "raw": {}, "meta": {}}, - ), - "delete_pull_request_comment_reaction": ( - "delete_issue_comment_reaction", - {"pull_request_id": "42", "comment_id": "99", "reaction_id": "5"}, - ("42", "99", "5"), - None, - ), - "get_pull_request_reactions": ( - "get_issue_reactions", - {"pull_request_id": "42", "pagination": {"cursor": "2", "per_page": 5}}, - ("42", {"cursor": "2", "per_page": 5}, None), - {"data": ["ok"], "type": "github", "raw": [], "meta": {"next_cursor": "3"}}, - ), - "create_pull_request_reaction": ( - "create_issue_reaction", - {"pull_request_id": "42", "reaction": "rocket"}, - ("42", "rocket"), - {"data": {"id": "1"}, "type": "github", "raw": {}, "meta": {}}, - ), - "delete_pull_request_reaction": ( - "delete_issue_reaction", - {"pull_request_id": "42", "reaction_id": "5"}, - ("42", "5"), - None, - ), -} - - -@pytest.mark.parametrize("case", PAGINATED_CASES) -def test_paginated_methods(case: dict[str, Any]) -> None: - provider, client = make_provider() - client.queue("get", FakeResponse(case["raw"])) - - result = getattr(provider, case["name"])(**case["kwargs"]) - - assert result["type"] == "github" - assert result["raw"] == {"data": case["raw"], "headers": {}} - assert result["data"] == case["expected_data"] - assert result["meta"] == {"next_cursor": case["next_cursor"]} - - assert client.calls == [ - { - "operation": "get", - "path": case["path"], - "params": case["params"], - "pagination": case["pagination"], - "request_options": None, - "extra_headers": None, - } - ] - - -@pytest.mark.parametrize("case", ACTION_CASES) -def test_action_methods(case: dict[str, Any]) -> None: - provider, client = make_provider() - client.queue( - case["operation"], - FakeResponse(case["raw"], headers=case.get("headers"), status_code=case.get("status_code")), - ) - - result = getattr(provider, case["name"])(**case["kwargs"]) - - assert result["type"] == "github" - assert result["raw"] == {"data": case["raw"], "headers": case.get("headers", {})} - assert result["data"] == case["expected_data"] - assert result["meta"] == {} - - expected_call = {"operation": case["operation"], "path": case["path"]} - if "data" in case: - expected_call["data"] = case["data"] - if case["operation"] == "get": - expected_call["params"] = case.get("params") - expected_call["pagination"] = None - expected_call["request_options"] = None - expected_call["extra_headers"] = None - else: - if "params" in case: - expected_call["params"] = case["params"] - expected_call["headers"] = case.get("headers") - assert client.calls == [expected_call] - - -def test_get_pull_request_diff_uses_raw_request_and_extracts_meta() -> None: - provider, client = make_provider() - response = FakeResponse( - {}, - headers={ - "ETag": '"etag-123"', - "Last-Modified": "Tue, 04 Feb 2026 10:00:00 GMT", - }, - text="diff --git a/f.py b/f.py", - ) - client.queue("get", response) - - result = provider.get_pull_request_diff("42") - - assert result["type"] == "github" - assert result["raw"] == { - "data": "diff --git a/f.py b/f.py", - "headers": { - "ETag": '"etag-123"', - "Last-Modified": "Tue, 04 Feb 2026 10:00:00 GMT", - }, - } - assert result["data"] == "diff --git a/f.py b/f.py" - assert result["meta"]["etag"] == '"etag-123"' - assert result["meta"]["last_modified"].isoformat() == "2026-02-04T10:00:00+00:00" - assert client.calls == [ - { - "operation": "get", - "path": "/repos/test-org/test-repo/pulls/42", - "params": None, - "pagination": None, - "request_options": None, - "extra_headers": {"Accept": "application/vnd.github.v3.diff"}, - } - ] - - -@pytest.mark.parametrize("case", VOID_CASES) -def test_void_methods(case: dict[str, Any]) -> None: - provider, client = make_provider() - client.queue(case["operation"], {} if case["operation"] == "graphql" else FakeResponse({})) - - result = getattr(provider, case["name"])(**case["kwargs"]) - - assert result is None - if case["operation"] == "graphql": - assert client.calls == [ - { - "operation": "graphql", - "query": case["query"], - "variables": case["variables"], - } - ] - elif case["operation"] == "post": - assert client.calls == [ - { - "operation": "post", - "path": case["path"], - "data": case["data"], - "headers": None, - } - ] - else: - assert client.calls == [{"operation": "delete", "path": case["path"]}] - - -@pytest.mark.parametrize("method_name", sorted(ALIAS_METHODS)) -def test_alias_methods_delegate_to_issue_methods(method_name: str) -> None: - delegated_name, kwargs, expected_args, expected_result = ALIAS_METHODS[method_name] - provider, _ = make_provider() - delegated = MagicMock(return_value=expected_result) - setattr(provider, delegated_name, delegated) - - result = getattr(provider, method_name)(**kwargs) - - delegated.assert_called_once_with(*expected_args) - assert result == expected_result - - -def test_provider_initialization_wraps_api_client() -> None: - raw_client = MagicMock(spec=GitHubApiClient) - repository = make_repository() - - provider = GitHubProvider( - raw_client, - organization_id=99, - repository=repository, - rate_limit_provider=NoOpRateLimitProvider(), - ) - - assert isinstance(provider.client, GitHubProviderApiClient) - assert provider.organization_id == 99 - assert provider.repository == repository - - -def test_is_rate_limited_returns_false() -> None: - provider, _ = make_provider() - - assert provider.is_rate_limited("shared") is False - - -def _make_api_client() -> GitHubProviderApiClient: - return GitHubProviderApiClient( - client=MagicMock(spec=GitHubApiClient), - organization_id=1, - rate_limit_provider=NoOpRateLimitProvider(), - ) - - -class TestGitHubProviderApiClientGraphql: - def test_returns_data_on_success(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse({"data": {"viewer": {"login": "octocat"}}}) - ) - - result = api_client.graphql("{ viewer { login } }", {}) - - assert result == {"viewer": {"login": "octocat"}} - api_client.post.assert_called_once_with( - "/graphql", data={"query": "{ viewer { login } }"}, headers={} - ) - - def test_includes_variables_when_provided(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse({"data": {"node": {"id": "123"}}}) - ) - - api_client.graphql("query($id: ID!) { node(id: $id) { id } }", {"id": "123"}) - - call_data = ( - api_client.post.call_args[1]["data"] - if api_client.post.call_args[1] - else api_client.post.call_args[0][1] - ) - assert call_data["variables"] == {"id": "123"} - - def test_excludes_variables_when_empty(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse({"data": {}}) - ) - - api_client.graphql("{ viewer { login } }", {}) - - call_data = ( - api_client.post.call_args[1]["data"] - if api_client.post.call_args[1] - else api_client.post.call_args[0][1] - ) - assert "variables" not in call_data - - def test_raises_on_non_dict_response(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse([{"unexpected": "list"}]) - ) - - with pytest.raises(SCMProviderException, match="not in expected format"): - api_client.graphql("{ viewer { login } }", {}) - - def test_raises_on_response_missing_data_and_errors(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse({"something": "else"}) - ) - - with pytest.raises(SCMProviderException, match="not in expected format"): - api_client.graphql("{ viewer { login } }", {}) - - def test_raises_on_errors_without_data(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse( - {"errors": [{"message": "Field not found"}, {"message": "Unauthorized"}]} - ) - ) - - with pytest.raises(SCMProviderException, match="Field not found\nUnauthorized"): - api_client.graphql("{ viewer { login } }", {}) - - def test_returns_data_on_partial_success_with_errors(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse( - { - "data": {"viewer": {"login": "octocat"}}, - "errors": [{"message": "Some warning"}], - } - ) - ) - - result = api_client.graphql("{ viewer { login } }", {}) - - assert result == {"viewer": {"login": "octocat"}} - - def test_returns_empty_dict_when_data_key_missing_but_errors_empty(self) -> None: - api_client = _make_api_client() - api_client.post = MagicMock( # type: ignore[method-assign] - return_value=FakeResponse({"errors": []}) - ) - - result = api_client.graphql("{ viewer { login } }", {}) - - assert result == {} - - -def test_public_methods_are_accounted_for() -> None: - covered_methods = { - "is_rate_limited", - "get_pull_request_diff", - *{case["name"] for case in PAGINATED_CASES}, - *{case["name"] for case in ACTION_CASES}, - *{case["name"] for case in VOID_CASES}, - *set(ALIAS_METHODS), - } - public_methods = { - name - for name, value in GitHubProvider.__dict__.items() - if callable(value) and not name.startswith("_") - } - - assert public_methods == covered_methods diff --git a/tests/sentry/scm/unit/test_gitlab_provider.py b/tests/sentry/scm/unit/test_gitlab_provider.py deleted file mode 100644 index b1ad8fe72f2280..00000000000000 --- a/tests/sentry/scm/unit/test_gitlab_provider.py +++ /dev/null @@ -1,13362 +0,0 @@ -import datetime -import unittest.mock -from collections.abc import Callable -from typing import Any, NamedTuple - -import pytest - -from sentry.integrations.gitlab.client import GitLabApiClient -from sentry.scm.private.providers.gitlab import GitLabProvider -from sentry.scm.types import Repository - - -@pytest.fixture -def client() -> GitLabApiClient: - return unittest.mock.create_autospec( - GitLabApiClient, spec_set=True, instance=True, _name="client" - ) - - -@pytest.fixture -def provider(client: GitLabApiClient) -> GitLabProvider: - return GitLabProvider( - client=client, - organization_id=1, - repository=Repository( - integration_id=1, - name="test-repo", - organization_id=1, - is_active=True, - external_id="gitlab.com:79787061", - ), - ) - - -class ClientForwardedCall(NamedTuple): - client_method: str - client_args: tuple - client_kwds: dict - client_return_value: Any - - -class ForwardToClientTest(NamedTuple): - provider_method: Callable - provider_args: dict - client_calls: list[ClientForwardedCall] - provider_return_value: Any - - -@pytest.mark.parametrize( - "param", - [ - # Actual calls made with an actual GitLab integration, captured for this file. - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_requests, - provider_args={ - "state": "open", - "head": None, - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_requests", - client_args=("79787061",), - client_kwds={"state": "opened"}, - client_return_value=[ - { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 47, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "459277081", - "number": "1", - "title": "Add blah", - "body": None, - "state": "open", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "head": { - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "ref": "topics/blah", - }, - "base": {"sha": None, "ref": "main"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 47, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request, - provider_args={"pull_request_id": "1", "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value={ - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 46, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - "subscribed": True, - "changes_count": "1", - "latest_build_started_at": None, - "latest_build_finished_at": None, - "first_deployed_to_production_at": None, - "pipeline": None, - "head_pipeline": None, - "diff_refs": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merge_error": None, - "first_contribution": True, - "user": {"can_merge": True}, - }, - ), - ], - provider_return_value={ - "data": { - "id": "459277081", - "number": "1", - "title": "Add blah", - "body": None, - "state": "open", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "head": { - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "ref": "topics/blah", - }, - "base": {"sha": None, "ref": "main"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 46, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - "subscribed": True, - "changes_count": "1", - "latest_build_started_at": None, - "latest_build_finished_at": None, - "first_deployed_to_production_at": None, - "pipeline": None, - "head_pipeline": None, - "diff_refs": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merge_error": None, - "first_contribution": True, - "user": {"can_merge": True}, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_pull_request, - provider_args={ - "title": "PR from API 2026-03-11 11:01:25.358068", - "body": "Another PR, made through the API.", - "head": "topics/blih", - "base": "main", - }, - client_calls=[ - ClientForwardedCall( - client_method="create_merge_request", - client_args=( - "79787061", - { - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "source_branch": "topics/blih", - "target_branch": "main", - }, - ), - client_kwds={}, - client_return_value={ - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "opened", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:25.811Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "checking", - "detailed_merge_status": "preparing", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": None, - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - "subscribed": True, - "changes_count": None, - "latest_build_started_at": None, - "latest_build_finished_at": None, - "first_deployed_to_production_at": None, - "pipeline": None, - "head_pipeline": None, - "diff_refs": None, - "merge_error": None, - "user": {"can_merge": True}, - }, - ), - ], - provider_return_value={ - "data": { - "id": "463099183", - "number": "26", - "title": "PR from API 2026-03-11 11:01:25.358068", - "body": "Another PR, made through the API.", - "state": "open", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "head": { - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "ref": "topics/blih", - }, - "base": {"sha": None, "ref": "main"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "opened", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:25.811Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "checking", - "detailed_merge_status": "preparing", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": None, - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - "subscribed": True, - "changes_count": None, - "latest_build_started_at": None, - "latest_build_finished_at": None, - "first_deployed_to_production_at": None, - "pipeline": None, - "head_pipeline": None, - "diff_refs": None, - "merge_error": None, - "user": {"can_merge": True}, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_requests, - provider_args={ - "state": "open", - "head": None, - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_requests", - client_args=("79787061",), - client_kwds={"state": "opened"}, - client_return_value=[ - { - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "opened", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:27.021Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T11:01:27.014Z", - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 47, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "463099183", - "number": "26", - "title": "PR from API 2026-03-11 11:01:25.358068", - "body": "Another PR, made through the API.", - "state": "open", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "head": { - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "ref": "topics/blih", - }, - "base": {"sha": None, "ref": "main"}, - }, - { - "id": "459277081", - "number": "1", - "title": "Add blah", - "body": None, - "state": "open", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "head": { - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "ref": "topics/blah", - }, - "base": {"sha": None, "ref": "main"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "opened", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:27.021Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T11:01:27.014Z", - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 47, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.update_pull_request, - provider_args={"pull_request_id": "26", "title": None, "body": None, "state": "closed"}, - client_calls=[ - ClientForwardedCall( - client_method="update_merge_request", - client_args=("79787061", "26", {"state_event": "close"}), - client_kwds={}, - client_return_value={ - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:36.336Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T11:01:36.166Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T11:01:27.014Z", - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - "subscribed": True, - "changes_count": "1", - "latest_build_started_at": None, - "latest_build_finished_at": None, - "first_deployed_to_production_at": None, - "pipeline": None, - "head_pipeline": None, - "diff_refs": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merge_error": None, - "user": {"can_merge": True}, - }, - ), - ], - provider_return_value={ - "data": { - "id": "463099183", - "number": "26", - "title": "PR from API 2026-03-11 11:01:25.358068", - "body": "Another PR, made through the API.", - "state": "closed", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "head": { - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "ref": "topics/blih", - }, - "base": {"sha": None, "ref": "main"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:36.336Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T11:01:36.166Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T11:01:27.014Z", - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - "subscribed": True, - "changes_count": "1", - "latest_build_started_at": None, - "latest_build_finished_at": None, - "first_deployed_to_production_at": None, - "pipeline": None, - "head_pipeline": None, - "diff_refs": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merge_error": None, - "user": {"can_merge": True}, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_requests, - provider_args={ - "state": "open", - "head": None, - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_requests", - client_args=("79787061",), - client_kwds={"state": "opened"}, - client_return_value=[ - { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 47, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "459277081", - "number": "1", - "title": "Add blah", - "body": None, - "state": "open", - "merged": False, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "head": { - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "ref": "topics/blah", - }, - "base": {"sha": None, "ref": "main"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 459277081, - "iid": 1, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "opened", - "created_at": "2026-02-26T08:48:23.151Z", - "updated_at": "2026-03-11T10:54:27.749Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": None, - "closed_at": None, - "target_branch": "main", - "source_branch": "topics/blah", - "user_notes_count": 47, - "upvotes": 0, - "downvotes": 1, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "mergeable", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-02-26T08:48:30.331Z", - "reference": "!1", - "references": { - "short": "!1", - "relative": "!1", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!1", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/1", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_commits, - provider_args={ - "ref": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_commits", - client_args=("79787061",), - client_kwds={"ref": "1403774c82d64068af027d0b5d0cc4f52473b6f2", "path": None}, - client_return_value=[ - { - "id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "short_id": "1403774c", - "created_at": "2026-02-16T14:24:18.000+01:00", - "parent_ids": [], - "title": "Initial commit", - "message": "Initial commit", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-16T14:24:18.000+01:00", - "committer_name": "GitHub", - "committer_email": "noreply@github.com", - "committed_date": "2026-02-16T14:24:18.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/1403774c82d64068af027d0b5d0cc4f52473b6f2", - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "message": "Initial commit", - "author": { - "name": "Vincent Jacques", - "email": "vincent@vincent-jacques.net", - "date": datetime.datetime( - 2026, - 2, - 16, - 14, - 24, - 18, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)), - ), - }, - "files": None, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "short_id": "1403774c", - "created_at": "2026-02-16T14:24:18.000+01:00", - "parent_ids": [], - "title": "Initial commit", - "message": "Initial commit", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-16T14:24:18.000+01:00", - "committer_name": "GitHub", - "committer_email": "noreply@github.com", - "committed_date": "2026-02-16T14:24:18.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/1403774c82d64068af027d0b5d0cc4f52473b6f2", - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_commits_by_path, - provider_args={ - "path": "src/main.py", - "ref": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_commits", - client_args=("79787061",), - client_kwds={ - "ref": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "path": "src/main.py", - }, - client_return_value=[ - { - "id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "short_id": "1403774c", - "created_at": "2026-02-16T14:24:18.000+01:00", - "parent_ids": [], - "title": "Initial commit", - "message": "Initial commit", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-16T14:24:18.000+01:00", - "committer_name": "GitHub", - "committer_email": "noreply@github.com", - "committed_date": "2026-02-16T14:24:18.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/1403774c82d64068af027d0b5d0cc4f52473b6f2", - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "message": "Initial commit", - "author": { - "name": "Vincent Jacques", - "email": "vincent@vincent-jacques.net", - "date": datetime.datetime( - 2026, - 2, - 16, - 14, - 24, - 18, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)), - ), - }, - "files": None, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "short_id": "1403774c", - "created_at": "2026-02-16T14:24:18.000+01:00", - "parent_ids": [], - "title": "Initial commit", - "message": "Initial commit", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-16T14:24:18.000+01:00", - "committer_name": "GitHub", - "committer_email": "noreply@github.com", - "committed_date": "2026-02-16T14:24:18.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/1403774c82d64068af027d0b5d0cc4f52473b6f2", - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_comments, - provider_args={"issue_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_notes", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 3123861269, - "type": None, - "body": "A comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T09:24:34.040Z", - "updated_at": "2026-03-11T10:51:17.829Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "3123861269", - "body": "A comment!", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 3123861269, - "type": None, - "body": "A comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T09:24:34.040Z", - "updated_at": "2026-03-11T10:51:17.829Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_issue_comment, - provider_args={"issue_id": "1", "body": "Another comment, made through the API."}, - client_calls=[ - ClientForwardedCall( - client_method="create_comment", - client_args=( - "79787061", - "1", - {"body": "Another comment, made through the API."}, - ), - client_kwds={}, - client_return_value={ - "id": 3149925511, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:12.644Z", - "updated_at": "2026-03-11T11:02:12.644Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ), - ], - provider_return_value={ - "data": { - "id": "3149925511", - "body": "Another comment, made through the API.", - "author": {"id": "150871", "username": "jacquev6"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 3149925511, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:12.644Z", - "updated_at": "2026-03-11T11:02:12.644Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_comments, - provider_args={"issue_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_notes", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 3149925511, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:12.644Z", - "updated_at": "2026-03-11T11:02:12.644Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3123861269, - "type": None, - "body": "A comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T09:24:34.040Z", - "updated_at": "2026-03-11T10:51:17.829Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "3149925511", - "body": "Another comment, made through the API.", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "3123861269", - "body": "A comment!", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 3149925511, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:12.644Z", - "updated_at": "2026-03-11T11:02:12.644Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3123861269, - "type": None, - "body": "A comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T09:24:34.040Z", - "updated_at": "2026-03-11T10:51:17.829Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.delete_issue_comment, - provider_args={"issue_id": "1", "comment_id": "3149925511"}, - client_calls=[ - ClientForwardedCall( - client_method="delete_issue_note", - client_args=("79787061", "1", "3149925511"), - client_kwds={}, - client_return_value={}, - ), - ], - provider_return_value=None, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_comments, - provider_args={"issue_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_notes", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 3123861269, - "type": None, - "body": "A comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T09:24:34.040Z", - "updated_at": "2026-03-11T10:51:17.829Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "3123861269", - "body": "A comment!", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 3123861269, - "type": None, - "body": "A comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T09:24:34.040Z", - "updated_at": "2026-03-11T10:51:17.829Z", - "system": False, - "noteable_id": 185129497, - "noteable_type": "Issue", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_comment_reactions, - provider_args={ - "issue_id": "1", - "comment_id": "3123861269", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_note_awards", - client_args=("79787061", "1", "3123861269"), - client_kwds={}, - client_return_value=[ - { - "id": 43909506, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:08.895Z", - "updated_at": "2026-03-02T10:42:08.895Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909515, - "name": "eyes", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:18.466Z", - "updated_at": "2026-03-02T10:42:18.466Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909523, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:28.940Z", - "updated_at": "2026-03-02T10:42:28.940Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911188, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:12:10.398Z", - "updated_at": "2026-03-02T11:12:10.398Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911265, - "name": "laughing", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:13:39.041Z", - "updated_at": "2026-03-02T11:13:39.041Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911283, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:08.025Z", - "updated_at": "2026-03-02T11:14:08.025Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911304, - "name": "confused", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:32.169Z", - "updated_at": "2026-03-02T11:14:32.169Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911321, - "name": "heart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:48.305Z", - "updated_at": "2026-03-02T11:14:48.305Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43909506", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43909515", - "content": "eyes", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911188", - "content": "-1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911265", - "content": "laugh", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911283", - "content": "hooray", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911304", - "content": "confused", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911321", - "content": "heart", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43909506, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:08.895Z", - "updated_at": "2026-03-02T10:42:08.895Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909515, - "name": "eyes", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:18.466Z", - "updated_at": "2026-03-02T10:42:18.466Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909523, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:28.940Z", - "updated_at": "2026-03-02T10:42:28.940Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911188, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:12:10.398Z", - "updated_at": "2026-03-02T11:12:10.398Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911265, - "name": "laughing", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:13:39.041Z", - "updated_at": "2026-03-02T11:13:39.041Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911283, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:08.025Z", - "updated_at": "2026-03-02T11:14:08.025Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911304, - "name": "confused", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:32.169Z", - "updated_at": "2026-03-02T11:14:32.169Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911321, - "name": "heart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:48.305Z", - "updated_at": "2026-03-02T11:14:48.305Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_issue_comment_reaction, - provider_args={"issue_id": "1", "comment_id": "3123861269", "reaction": "rocket"}, - client_calls=[ - ClientForwardedCall( - client_method="create_issue_note_award", - client_args=("79787061", "1", "3123861269", "rocket"), - client_kwds={}, - client_return_value={ - "id": 44362880, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:42.934Z", - "updated_at": "2026-03-11T11:02:42.934Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ), - ], - provider_return_value={ - "data": { - "id": "44362880", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 44362880, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:42.934Z", - "updated_at": "2026-03-11T11:02:42.934Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_comment_reactions, - provider_args={ - "issue_id": "1", - "comment_id": "3123861269", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_note_awards", - client_args=("79787061", "1", "3123861269"), - client_kwds={}, - client_return_value=[ - { - "id": 43909506, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:08.895Z", - "updated_at": "2026-03-02T10:42:08.895Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909515, - "name": "eyes", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:18.466Z", - "updated_at": "2026-03-02T10:42:18.466Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909523, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:28.940Z", - "updated_at": "2026-03-02T10:42:28.940Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911188, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:12:10.398Z", - "updated_at": "2026-03-02T11:12:10.398Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911265, - "name": "laughing", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:13:39.041Z", - "updated_at": "2026-03-02T11:13:39.041Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911283, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:08.025Z", - "updated_at": "2026-03-02T11:14:08.025Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911304, - "name": "confused", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:32.169Z", - "updated_at": "2026-03-02T11:14:32.169Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911321, - "name": "heart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:48.305Z", - "updated_at": "2026-03-02T11:14:48.305Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 44362880, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:42.934Z", - "updated_at": "2026-03-11T11:02:42.934Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43909506", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43909515", - "content": "eyes", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911188", - "content": "-1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911265", - "content": "laugh", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911283", - "content": "hooray", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911304", - "content": "confused", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911321", - "content": "heart", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "44362880", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43909506, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:08.895Z", - "updated_at": "2026-03-02T10:42:08.895Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909515, - "name": "eyes", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:18.466Z", - "updated_at": "2026-03-02T10:42:18.466Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909523, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:28.940Z", - "updated_at": "2026-03-02T10:42:28.940Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911188, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:12:10.398Z", - "updated_at": "2026-03-02T11:12:10.398Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911265, - "name": "laughing", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:13:39.041Z", - "updated_at": "2026-03-02T11:13:39.041Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911283, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:08.025Z", - "updated_at": "2026-03-02T11:14:08.025Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911304, - "name": "confused", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:32.169Z", - "updated_at": "2026-03-02T11:14:32.169Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911321, - "name": "heart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:48.305Z", - "updated_at": "2026-03-02T11:14:48.305Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 44362880, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:02:42.934Z", - "updated_at": "2026-03-11T11:02:42.934Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.delete_issue_comment_reaction, - provider_args={"issue_id": "1", "comment_id": "3123861269", "reaction_id": "44362880"}, - client_calls=[ - ClientForwardedCall( - client_method="delete_issue_note_award", - client_args=("79787061", "1", "3123861269", "44362880"), - client_kwds={}, - client_return_value={}, - ), - ], - provider_return_value=None, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_comment_reactions, - provider_args={ - "issue_id": "1", - "comment_id": "3123861269", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_note_awards", - client_args=("79787061", "1", "3123861269"), - client_kwds={}, - client_return_value=[ - { - "id": 43909506, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:08.895Z", - "updated_at": "2026-03-02T10:42:08.895Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909515, - "name": "eyes", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:18.466Z", - "updated_at": "2026-03-02T10:42:18.466Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909523, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:28.940Z", - "updated_at": "2026-03-02T10:42:28.940Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911188, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:12:10.398Z", - "updated_at": "2026-03-02T11:12:10.398Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911265, - "name": "laughing", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:13:39.041Z", - "updated_at": "2026-03-02T11:13:39.041Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911283, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:08.025Z", - "updated_at": "2026-03-02T11:14:08.025Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911304, - "name": "confused", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:32.169Z", - "updated_at": "2026-03-02T11:14:32.169Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911321, - "name": "heart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:48.305Z", - "updated_at": "2026-03-02T11:14:48.305Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43909506", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43909515", - "content": "eyes", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911188", - "content": "-1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911265", - "content": "laugh", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911283", - "content": "hooray", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911304", - "content": "confused", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43911321", - "content": "heart", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43909506, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:08.895Z", - "updated_at": "2026-03-02T10:42:08.895Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909515, - "name": "eyes", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:18.466Z", - "updated_at": "2026-03-02T10:42:18.466Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43909523, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T10:42:28.940Z", - "updated_at": "2026-03-02T10:42:28.940Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911188, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:12:10.398Z", - "updated_at": "2026-03-02T11:12:10.398Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911265, - "name": "laughing", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:13:39.041Z", - "updated_at": "2026-03-02T11:13:39.041Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911283, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:08.025Z", - "updated_at": "2026-03-02T11:14:08.025Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911304, - "name": "confused", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:32.169Z", - "updated_at": "2026-03-02T11:14:32.169Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43911321, - "name": "heart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T11:14:48.305Z", - "updated_at": "2026-03-02T11:14:48.305Z", - "awardable_id": 3123861269, - "awardable_type": "Note", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_comments, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_notes", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 3149884824, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:39.481Z", - "updated_at": "2026-03-11T10:54:39.487Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884711, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:54:28.116590.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:37.587Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149883989, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:54:26.720889.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:27.718Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149861666, - "type": None, - "body": "A great comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:49:16.764Z", - "updated_at": "2026-03-11T10:49:23.286Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149860409, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:48:59.701Z", - "updated_at": "2026-03-11T10:48:59.706Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830983, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:41:39.384210.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:48.803Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830400, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:41:37.997252.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:38.931Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614637, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:46:20.040845.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:29.524Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614105, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:46:18.421280.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:19.570Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593898, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:41:29.673519.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:39.085Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593131, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:41:28.208433.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:29.065Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374674, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 08:40:41.708400.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:50.641Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374039, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 08:40:38.693706.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:40.323Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056679, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:16:46.431900.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:55.897Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056453, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:16:45.103757.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:45.970Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030774, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:00:18.900904.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:28.506Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030578, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:00:17.604102.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:18.211Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3146555287, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T14:41:56.638Z", - "updated_at": "2026-03-10T14:41:56.642Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802996, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-10 11:11:36.111214.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:36.929Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802877, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-10 11:11:34.526408.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:35.508Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "3149861666", - "body": "A great comment!", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 3149884824, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:39.481Z", - "updated_at": "2026-03-11T10:54:39.487Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884711, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:54:28.116590.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:37.587Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149883989, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:54:26.720889.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:27.718Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149861666, - "type": None, - "body": "A great comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:49:16.764Z", - "updated_at": "2026-03-11T10:49:23.286Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149860409, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:48:59.701Z", - "updated_at": "2026-03-11T10:48:59.706Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830983, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:41:39.384210.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:48.803Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830400, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:41:37.997252.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:38.931Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614637, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:46:20.040845.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:29.524Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614105, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:46:18.421280.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:19.570Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593898, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:41:29.673519.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:39.085Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593131, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:41:28.208433.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:29.065Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374674, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 08:40:41.708400.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:50.641Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374039, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 08:40:38.693706.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:40.323Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056679, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:16:46.431900.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:55.897Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056453, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:16:45.103757.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:45.970Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030774, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:00:18.900904.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:28.506Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030578, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:00:17.604102.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:18.211Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3146555287, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T14:41:56.638Z", - "updated_at": "2026-03-10T14:41:56.642Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802996, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-10 11:11:36.111214.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:36.929Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802877, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-10 11:11:34.526408.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:35.508Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_pull_request_comment, - provider_args={ - "pull_request_id": "1", - "body": "Another comment, made through the API.", - }, - client_calls=[ - ClientForwardedCall( - client_method="create_merge_request_note", - client_args=( - "79787061", - "1", - {"body": "Another comment, made through the API."}, - ), - client_kwds={}, - client_return_value={ - "id": 3149931216, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:13.116Z", - "updated_at": "2026-03-11T11:03:13.116Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ), - ], - provider_return_value={ - "data": { - "id": "3149931216", - "body": "Another comment, made through the API.", - "author": {"id": "150871", "username": "jacquev6"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 3149931216, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:13.116Z", - "updated_at": "2026-03-11T11:03:13.116Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_comments, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_notes", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 3149931216, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:13.116Z", - "updated_at": "2026-03-11T11:03:13.116Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884824, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:39.481Z", - "updated_at": "2026-03-11T10:54:39.487Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884711, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:54:28.116590.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:37.587Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149883989, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:54:26.720889.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:27.718Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149861666, - "type": None, - "body": "A great comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:49:16.764Z", - "updated_at": "2026-03-11T10:49:23.286Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149860409, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:48:59.701Z", - "updated_at": "2026-03-11T10:48:59.706Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830983, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:41:39.384210.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:48.803Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830400, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:41:37.997252.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:38.931Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614637, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:46:20.040845.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:29.524Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614105, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:46:18.421280.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:19.570Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593898, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:41:29.673519.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:39.085Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593131, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:41:28.208433.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:29.065Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374674, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 08:40:41.708400.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:50.641Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374039, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 08:40:38.693706.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:40.323Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056679, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:16:46.431900.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:55.897Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056453, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:16:45.103757.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:45.970Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030774, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:00:18.900904.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:28.506Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030578, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:00:17.604102.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:18.211Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3146555287, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T14:41:56.638Z", - "updated_at": "2026-03-10T14:41:56.642Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802996, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-10 11:11:36.111214.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:36.929Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "3149931216", - "body": "Another comment, made through the API.", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "3149861666", - "body": "A great comment!", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 3149931216, - "type": None, - "body": "Another comment, made through the API.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:13.116Z", - "updated_at": "2026-03-11T11:03:13.116Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884824, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:39.481Z", - "updated_at": "2026-03-11T10:54:39.487Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884711, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:54:28.116590.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:37.587Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149883989, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:54:26.720889.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:27.718Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149861666, - "type": None, - "body": "A great comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:49:16.764Z", - "updated_at": "2026-03-11T10:49:23.286Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149860409, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:48:59.701Z", - "updated_at": "2026-03-11T10:48:59.706Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830983, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:41:39.384210.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:48.803Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830400, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:41:37.997252.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:38.931Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614637, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:46:20.040845.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:29.524Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614105, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:46:18.421280.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:19.570Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593898, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:41:29.673519.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:39.085Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593131, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:41:28.208433.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:29.065Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374674, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 08:40:41.708400.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:50.641Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374039, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 08:40:38.693706.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:40.323Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056679, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:16:46.431900.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:55.897Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056453, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:16:45.103757.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:45.970Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030774, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:00:18.900904.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:28.506Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030578, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:00:17.604102.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:18.211Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3146555287, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T14:41:56.638Z", - "updated_at": "2026-03-10T14:41:56.642Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802996, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-10 11:11:36.111214.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:36.929Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.delete_pull_request_comment, - provider_args={"pull_request_id": "1", "comment_id": "3149931216"}, - client_calls=[ - ClientForwardedCall( - client_method="delete_merge_request_note", - client_args=("79787061", "1", "3149931216"), - client_kwds={}, - client_return_value={}, - ), - ], - provider_return_value=None, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_comments, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_notes", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 3149884824, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:39.481Z", - "updated_at": "2026-03-11T10:54:39.487Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884711, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:54:28.116590.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:37.587Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149883989, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:54:26.720889.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:27.718Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149861666, - "type": None, - "body": "A great comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:49:16.764Z", - "updated_at": "2026-03-11T10:49:23.286Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149860409, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:48:59.701Z", - "updated_at": "2026-03-11T10:48:59.706Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830983, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:41:39.384210.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:48.803Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830400, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:41:37.997252.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:38.931Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614637, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:46:20.040845.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:29.524Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614105, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:46:18.421280.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:19.570Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593898, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:41:29.673519.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:39.085Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593131, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:41:28.208433.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:29.065Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374674, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 08:40:41.708400.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:50.641Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374039, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 08:40:38.693706.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:40.323Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056679, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:16:46.431900.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:55.897Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056453, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:16:45.103757.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:45.970Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030774, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:00:18.900904.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:28.506Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030578, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:00:17.604102.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:18.211Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3146555287, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T14:41:56.638Z", - "updated_at": "2026-03-10T14:41:56.642Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802996, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-10 11:11:36.111214.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:36.929Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802877, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-10 11:11:34.526408.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:35.508Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "3149861666", - "body": "A great comment!", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 3149884824, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:39.481Z", - "updated_at": "2026-03-11T10:54:39.487Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149884711, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:54:28.116590.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:37.587Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149883989, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:54:26.720889.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:54:27.718Z", - "updated_at": "2026-03-11T10:54:39.420Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:54:39.420Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149861666, - "type": None, - "body": "A great comment!", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:49:16.764Z", - "updated_at": "2026-03-11T10:49:23.286Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149860409, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:48:59.701Z", - "updated_at": "2026-03-11T10:48:59.706Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830983, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 10:41:39.384210.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:48.803Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149830400, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 10:41:37.997252.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T10:41:38.931Z", - "updated_at": "2026-03-11T10:48:59.635Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:59.635Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614637, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:46:20.040845.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:29.524Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149614105, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:46:18.421280.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:46:19.570Z", - "updated_at": "2026-03-11T10:48:58.011Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:58.011Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593898, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 09:41:29.673519.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:39.085Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149593131, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 09:41:28.208433.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T09:41:29.065Z", - "updated_at": "2026-03-11T10:48:55.813Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:55.813Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374674, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 08:40:41.708400.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:50.641Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149374039, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 08:40:38.693706.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T08:40:40.323Z", - "updated_at": "2026-03-11T10:48:54.026Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:54.026Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056679, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:16:46.431900.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:55.897Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149056453, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:16:45.103757.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:16:45.970Z", - "updated_at": "2026-03-11T10:48:52.678Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:52.678Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030774, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 06:00:18.900904.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:28.506Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3149030578, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 06:00:17.604102.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T06:00:18.211Z", - "updated_at": "2026-03-11T10:48:50.527Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-11T10:48:50.527Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3146555287, - "type": None, - "body": "resolved all threads", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T14:41:56.638Z", - "updated_at": "2026-03-10T14:41:56.642Z", - "system": True, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "resolvable": False, - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802996, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-10 11:11:36.111214.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:36.929Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - { - "id": 3145802877, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-10 11:11:34.526408.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-10T11:11:35.508Z", - "updated_at": "2026-03-10T14:41:52.922Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": True, - "resolved_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "resolved_at": "2026-03-10T14:41:52.922Z", - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_comment_reactions, - provider_args={ - "pull_request_id": "1", - "comment_id": "3124015530", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_note_awards", - client_args=("79787061", "1", "3124015530"), - client_kwds={}, - client_return_value=[ - { - "id": 43921665, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:16.980Z", - "updated_at": "2026-03-02T14:23:16.980Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43921671, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:20.949Z", - "updated_at": "2026-03-02T14:23:20.949Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43921665", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43921665, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:16.980Z", - "updated_at": "2026-03-02T14:23:16.980Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43921671, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:20.949Z", - "updated_at": "2026-03-02T14:23:20.949Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_pull_request_comment_reaction, - provider_args={ - "pull_request_id": "1", - "comment_id": "3124015530", - "reaction": "rocket", - }, - client_calls=[ - ClientForwardedCall( - client_method="create_merge_request_note_award", - client_args=("79787061", "1", "3124015530", "rocket"), - client_kwds={}, - client_return_value={ - "id": 44362951, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:43.499Z", - "updated_at": "2026-03-11T11:03:43.499Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ), - ], - provider_return_value={ - "data": { - "id": "44362951", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 44362951, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:43.499Z", - "updated_at": "2026-03-11T11:03:43.499Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_comment_reactions, - provider_args={ - "pull_request_id": "1", - "comment_id": "3124015530", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_note_awards", - client_args=("79787061", "1", "3124015530"), - client_kwds={}, - client_return_value=[ - { - "id": 43921665, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:16.980Z", - "updated_at": "2026-03-02T14:23:16.980Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43921671, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:20.949Z", - "updated_at": "2026-03-02T14:23:20.949Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 44362951, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:43.499Z", - "updated_at": "2026-03-11T11:03:43.499Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43921665", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "44362951", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43921665, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:16.980Z", - "updated_at": "2026-03-02T14:23:16.980Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43921671, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:20.949Z", - "updated_at": "2026-03-02T14:23:20.949Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 44362951, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:03:43.499Z", - "updated_at": "2026-03-11T11:03:43.499Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.delete_pull_request_comment_reaction, - provider_args={ - "pull_request_id": "1", - "comment_id": "3124015530", - "reaction_id": "44362951", - }, - client_calls=[ - ClientForwardedCall( - client_method="delete_merge_request_note_award", - client_args=("79787061", "1", "3124015530", "44362951"), - client_kwds={}, - client_return_value={}, - ), - ], - provider_return_value=None, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_comment_reactions, - provider_args={ - "pull_request_id": "1", - "comment_id": "3124015530", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_note_awards", - client_args=("79787061", "1", "3124015530"), - client_kwds={}, - client_return_value=[ - { - "id": 43921665, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:16.980Z", - "updated_at": "2026-03-02T14:23:16.980Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43921671, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:20.949Z", - "updated_at": "2026-03-02T14:23:20.949Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43921665", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43921665, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:16.980Z", - "updated_at": "2026-03-02T14:23:16.980Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - { - "id": 43921671, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:23:20.949Z", - "updated_at": "2026-03-02T14:23:20.949Z", - "awardable_id": 3124015530, - "awardable_type": "Note", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_reactions, - provider_args={"issue_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_awards", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 43923647, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:40.410Z", - "updated_at": "2026-03-02T14:52:40.410Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923656, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:47.638Z", - "updated_at": "2026-03-02T14:52:47.638Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923674, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:53:00.711Z", - "updated_at": "2026-03-02T14:53:00.711Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43923647", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43923674", - "content": "hooray", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43923647, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:40.410Z", - "updated_at": "2026-03-02T14:52:40.410Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923656, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:47.638Z", - "updated_at": "2026-03-02T14:52:47.638Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923674, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:53:00.711Z", - "updated_at": "2026-03-02T14:53:00.711Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_issue_reaction, - provider_args={"issue_id": "1", "reaction": "rocket"}, - client_calls=[ - ClientForwardedCall( - client_method="create_issue_award", - client_args=("79787061", "1", "rocket"), - client_kwds={}, - client_return_value={ - "id": 44362995, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:13.218Z", - "updated_at": "2026-03-11T11:04:13.218Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ), - ], - provider_return_value={ - "data": { - "id": "44362995", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 44362995, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:13.218Z", - "updated_at": "2026-03-11T11:04:13.218Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_reactions, - provider_args={"issue_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_awards", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 43923647, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:40.410Z", - "updated_at": "2026-03-02T14:52:40.410Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923656, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:47.638Z", - "updated_at": "2026-03-02T14:52:47.638Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923674, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:53:00.711Z", - "updated_at": "2026-03-02T14:53:00.711Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 44362995, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:13.218Z", - "updated_at": "2026-03-11T11:04:13.218Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43923647", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43923674", - "content": "hooray", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "44362995", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43923647, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:40.410Z", - "updated_at": "2026-03-02T14:52:40.410Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923656, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:47.638Z", - "updated_at": "2026-03-02T14:52:47.638Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923674, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:53:00.711Z", - "updated_at": "2026-03-02T14:53:00.711Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 44362995, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:13.218Z", - "updated_at": "2026-03-11T11:04:13.218Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.delete_issue_reaction, - provider_args={"issue_id": "1", "reaction_id": "44362995"}, - client_calls=[ - ClientForwardedCall( - client_method="delete_issue_award", - client_args=("79787061", "1", "44362995"), - client_kwds={}, - client_return_value={}, - ), - ], - provider_return_value=None, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_issue_reactions, - provider_args={"issue_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_issue_awards", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 43923647, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:40.410Z", - "updated_at": "2026-03-02T14:52:40.410Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923656, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:47.638Z", - "updated_at": "2026-03-02T14:52:47.638Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923674, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:53:00.711Z", - "updated_at": "2026-03-02T14:53:00.711Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43923647", - "content": "+1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "43923674", - "content": "hooray", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43923647, - "name": "thumbsup", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:40.410Z", - "updated_at": "2026-03-02T14:52:40.410Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923656, - "name": "dart", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:52:47.638Z", - "updated_at": "2026-03-02T14:52:47.638Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - { - "id": 43923674, - "name": "tada", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T14:53:00.711Z", - "updated_at": "2026-03-02T14:53:00.711Z", - "awardable_id": 185129497, - "awardable_type": "Issue", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_reactions, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_awards", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 43924243, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:37.869Z", - "updated_at": "2026-03-02T15:00:37.869Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 43924251, - "name": "smiling_face_with_hearts", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:43.985Z", - "updated_at": "2026-03-02T15:00:43.985Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43924243", - "content": "-1", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43924243, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:37.869Z", - "updated_at": "2026-03-02T15:00:37.869Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 43924251, - "name": "smiling_face_with_hearts", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:43.985Z", - "updated_at": "2026-03-02T15:00:43.985Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_pull_request_reaction, - provider_args={"pull_request_id": "1", "reaction": "rocket"}, - client_calls=[ - ClientForwardedCall( - client_method="create_merge_request_award", - client_args=("79787061", "1", "rocket"), - client_kwds={}, - client_return_value={ - "id": 44363033, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:43.769Z", - "updated_at": "2026-03-11T11:04:43.769Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ), - ], - provider_return_value={ - "data": { - "id": "44363033", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - "type": "gitlab", - "raw": { - "data": { - "id": 44363033, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:43.769Z", - "updated_at": "2026-03-11T11:04:43.769Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_reactions, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_awards", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 43924243, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:37.869Z", - "updated_at": "2026-03-02T15:00:37.869Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 43924251, - "name": "smiling_face_with_hearts", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:43.985Z", - "updated_at": "2026-03-02T15:00:43.985Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 44363033, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:43.769Z", - "updated_at": "2026-03-11T11:04:43.769Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43924243", - "content": "-1", - "author": {"id": "150871", "username": "jacquev6"}, - }, - { - "id": "44363033", - "content": "rocket", - "author": {"id": "150871", "username": "jacquev6"}, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43924243, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:37.869Z", - "updated_at": "2026-03-02T15:00:37.869Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 43924251, - "name": "smiling_face_with_hearts", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:43.985Z", - "updated_at": "2026-03-02T15:00:43.985Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 44363033, - "name": "rocket", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:04:43.769Z", - "updated_at": "2026-03-11T11:04:43.769Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.delete_pull_request_reaction, - provider_args={"pull_request_id": "1", "reaction_id": "44363033"}, - client_calls=[ - ClientForwardedCall( - client_method="delete_merge_request_award", - client_args=("79787061", "1", "44363033"), - client_kwds={}, - client_return_value={}, - ), - ], - provider_return_value=None, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_reactions, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_awards", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 43924243, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:37.869Z", - "updated_at": "2026-03-02T15:00:37.869Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 43924251, - "name": "smiling_face_with_hearts", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:43.985Z", - "updated_at": "2026-03-02T15:00:43.985Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "43924243", - "content": "-1", - "author": {"id": "150871", "username": "jacquev6"}, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 43924243, - "name": "thumbsdown", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:37.869Z", - "updated_at": "2026-03-02T15:00:37.869Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - { - "id": 43924251, - "name": "smiling_face_with_hearts", - "user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-02T15:00:43.985Z", - "updated_at": "2026-03-02T15:00:43.985Z", - "awardable_id": 459277081, - "awardable_type": "MergeRequest", - "url": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_branch, - provider_args={"branch": "topics/blah", "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_branch", - client_args=("79787061", "topics/blah"), - client_kwds={}, - client_return_value={ - "name": "topics/blah", - "commit": { - "id": "7497e018d01503b6abc3053b7896266115e631f6", - "short_id": "7497e018", - "created_at": "2026-03-05T12:15:50.000+01:00", - "parent_ids": ["6d8ca33dae268d3c5835e721e5702ef9dcb43c8c"], - "title": "Add content", - "message": "Add content\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-03-05T12:15:50.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-03-05T12:15:50.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/7497e018d01503b6abc3053b7896266115e631f6", - }, - "merged": False, - "protected": False, - "developers_can_push": False, - "developers_can_merge": False, - "can_push": True, - "default": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/tree/topics/blah", - }, - ), - ], - provider_return_value={ - "data": {"ref": "topics/blah", "sha": "7497e018d01503b6abc3053b7896266115e631f6"}, - "type": "gitlab", - "raw": { - "data": { - "name": "topics/blah", - "commit": { - "id": "7497e018d01503b6abc3053b7896266115e631f6", - "short_id": "7497e018", - "created_at": "2026-03-05T12:15:50.000+01:00", - "parent_ids": ["6d8ca33dae268d3c5835e721e5702ef9dcb43c8c"], - "title": "Add content", - "message": "Add content\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-03-05T12:15:50.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-03-05T12:15:50.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/7497e018d01503b6abc3053b7896266115e631f6", - }, - "merged": False, - "protected": False, - "developers_can_push": False, - "developers_can_merge": False, - "can_push": True, - "default": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/tree/topics/blah", - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_branch, - provider_args={ - "branch": "tests/20260311-110504", - "sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - client_calls=[ - ClientForwardedCall( - client_method="create_branch", - client_args=( - "79787061", - "tests/20260311-110504", - "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - ), - client_kwds={}, - client_return_value={ - "name": "tests/20260311-110504", - "commit": { - "id": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "short_id": "0941ee0a", - "created_at": "2026-02-26T09:01:54.000+01:00", - "parent_ids": ["1403774c82d64068af027d0b5d0cc4f52473b6f2"], - "title": "Another", - "message": "Another\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:01:54.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:01:54.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merged": False, - "protected": False, - "developers_can_push": False, - "developers_can_merge": False, - "can_push": True, - "default": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/tree/tests/20260311-110504", - }, - ), - ], - provider_return_value={ - "data": { - "ref": "tests/20260311-110504", - "sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "type": "gitlab", - "raw": { - "data": { - "name": "tests/20260311-110504", - "commit": { - "id": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "short_id": "0941ee0a", - "created_at": "2026-02-26T09:01:54.000+01:00", - "parent_ids": ["1403774c82d64068af027d0b5d0cc4f52473b6f2"], - "title": "Another", - "message": "Another\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:01:54.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:01:54.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merged": False, - "protected": False, - "developers_can_push": False, - "developers_can_merge": False, - "can_push": True, - "default": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/tree/tests/20260311-110504", - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_branch, - provider_args={"branch": "tests/20260311-110504", "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_branch", - client_args=("79787061", "tests/20260311-110504"), - client_kwds={}, - client_return_value={ - "name": "tests/20260311-110504", - "commit": { - "id": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "short_id": "0941ee0a", - "created_at": "2026-02-26T09:01:54.000+01:00", - "parent_ids": ["1403774c82d64068af027d0b5d0cc4f52473b6f2"], - "title": "Another", - "message": "Another\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:01:54.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:01:54.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merged": False, - "protected": False, - "developers_can_push": False, - "developers_can_merge": False, - "can_push": True, - "default": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/tree/tests/20260311-110504", - }, - ), - ], - provider_return_value={ - "data": { - "ref": "tests/20260311-110504", - "sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "type": "gitlab", - "raw": { - "data": { - "name": "tests/20260311-110504", - "commit": { - "id": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "short_id": "0941ee0a", - "created_at": "2026-02-26T09:01:54.000+01:00", - "parent_ids": ["1403774c82d64068af027d0b5d0cc4f52473b6f2"], - "title": "Another", - "message": "Another\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:01:54.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:01:54.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - }, - "merged": False, - "protected": False, - "developers_can_push": False, - "developers_can_merge": False, - "can_push": True, - "default": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/tree/tests/20260311-110504", - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_file_content, - provider_args={"path": "README.md", "ref": "main", "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_file_content", - client_args=("79787061", "README.md", "main"), - client_kwds={}, - client_return_value={ - "file_name": "README.md", - "file_path": "README.md", - "size": 92, - "encoding": "base64", - "content_sha256": "34ecd6d09e3a9b3c386fcac36a83339c744154b3713d8d44d75fb0cd82f0b26a", - "ref": "main", - "blob_id": "d96986775b6793cac0a358b35650de94752a9530", - "commit_id": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "last_commit_id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "execute_filemode": False, - "content": "IyB0ZXN0LVNlbnRyeS1JbnRlZ3JhdGlvbi1EZXYtamFjcXVldjYKVGVzdCByZXBvIGZvciBteSBkZXZlbG9wbWVudHMgaW4gU2VudHJ5J3MgR2l0SHViIEFwcAo=", - }, - ), - ], - provider_return_value={ - "data": { - "path": "README.md", - "sha": "d96986775b6793cac0a358b35650de94752a9530", - "content": "IyB0ZXN0LVNlbnRyeS1JbnRlZ3JhdGlvbi1EZXYtamFjcXVldjYKVGVzdCByZXBvIGZvciBteSBkZXZlbG9wbWVudHMgaW4gU2VudHJ5J3MgR2l0SHViIEFwcAo=", - "encoding": "base64", - "size": 92, - }, - "type": "gitlab", - "raw": { - "data": { - "file_name": "README.md", - "file_path": "README.md", - "size": 92, - "encoding": "base64", - "content_sha256": "34ecd6d09e3a9b3c386fcac36a83339c744154b3713d8d44d75fb0cd82f0b26a", - "ref": "main", - "blob_id": "d96986775b6793cac0a358b35650de94752a9530", - "commit_id": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "last_commit_id": "1403774c82d64068af027d0b5d0cc4f52473b6f2", - "execute_filemode": False, - "content": "IyB0ZXN0LVNlbnRyeS1JbnRlZ3JhdGlvbi1EZXYtamFjcXVldjYKVGVzdCByZXBvIGZvciBteSBkZXZlbG9wbWVudHMgaW4gU2VudHJ5J3MgR2l0SHViIEFwcAo=", - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.compare_commits, - provider_args={ - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "end_sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="compare_commits", - client_args=( - "79787061", - "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - ), - client_kwds={}, - client_return_value={ - "commit": { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - "commits": [ - { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - } - ], - "diffs": [ - { - "diff": "", - "collapsed": False, - "too_large": False, - "new_path": "BLAH.md", - "old_path": "BLAH.md", - "a_mode": "0", - "b_mode": "100644", - "new_file": True, - "renamed_file": False, - "deleted_file": False, - "generated_file": None, - } - ], - "compare_timeout": False, - "compare_same_ref": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/compare/0941ee0a9eac9914cfddf5adec7a9558a2f1c447...6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - ), - ], - provider_return_value={ - "data": [ - { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "message": "Add blah\n", - "author": { - "name": "Vincent Jacques", - "email": "vincent@vincent-jacques.net", - "date": datetime.datetime( - 2026, - 2, - 26, - 9, - 47, - 45, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)), - ), - }, - "files": None, - } - ], - "type": "gitlab", - "raw": { - "data": { - "commit": { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - "commits": [ - { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - } - ], - "diffs": [ - { - "diff": "", - "collapsed": False, - "too_large": False, - "new_path": "BLAH.md", - "old_path": "BLAH.md", - "a_mode": "0", - "b_mode": "100644", - "new_file": True, - "renamed_file": False, - "deleted_file": False, - "generated_file": None, - } - ], - "compare_timeout": False, - "compare_same_ref": False, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/compare/0941ee0a9eac9914cfddf5adec7a9558a2f1c447...6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_commit, - provider_args={ - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_commit", - client_args=("79787061", "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c"), - client_kwds={}, - client_return_value={ - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "stats": {"additions": 0, "deletions": 0, "total": 0}, - "status": None, - "project_id": 79787061, - "last_pipeline": None, - }, - ), - ], - provider_return_value={ - "data": { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "message": "Add blah\n", - "author": { - "name": "Vincent Jacques", - "email": "vincent@vincent-jacques.net", - "date": datetime.datetime( - 2026, - 2, - 26, - 9, - 47, - 45, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)), - ), - }, - "files": None, - }, - "type": "gitlab", - "raw": { - "data": { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "stats": {"additions": 0, "deletions": 0, "total": 0}, - "status": None, - "project_id": 79787061, - "last_pipeline": None, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_files, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_diffs", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "diff": "@@ -0,0 +1,9 @@\n+1\n+2\n+3\n+4\n+5\n+6\n+7\n+8\n+9\n", - "collapsed": False, - "too_large": False, - "new_path": "BLAH.md", - "old_path": "BLAH.md", - "a_mode": "0", - "b_mode": "100644", - "new_file": True, - "renamed_file": False, - "deleted_file": False, - "generated_file": False, - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "filename": "BLAH.md", - "status": "added", - "patch": "@@ -0,0 +1,9 @@\n+1\n+2\n+3\n+4\n+5\n+6\n+7\n+8\n+9\n", - "changes": 0, - "sha": "", - "previous_filename": None, - } - ], - "type": "gitlab", - "raw": { - "data": [ - { - "diff": "@@ -0,0 +1,9 @@\n+1\n+2\n+3\n+4\n+5\n+6\n+7\n+8\n+9\n", - "collapsed": False, - "too_large": False, - "new_path": "BLAH.md", - "old_path": "BLAH.md", - "a_mode": "0", - "b_mode": "100644", - "new_file": True, - "renamed_file": False, - "deleted_file": False, - "generated_file": False, - } - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_request_commits, - provider_args={"pull_request_id": "1", "pagination": None, "request_options": None}, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_commits", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": "7497e018d01503b6abc3053b7896266115e631f6", - "short_id": "7497e018", - "created_at": "2026-03-05T12:15:50.000+01:00", - "parent_ids": ["6d8ca33dae268d3c5835e721e5702ef9dcb43c8c"], - "title": "Add content", - "message": "Add content\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-03-05T12:15:50.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-03-05T12:15:50.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/7497e018d01503b6abc3053b7896266115e631f6", - }, - { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - ], - ), - ], - provider_return_value={ - "data": [ - { - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "message": "Add blah\n", - "author": { - "name": "Vincent Jacques", - "email": "vincent@vincent-jacques.net", - "date": datetime.datetime( - 2026, - 2, - 26, - 9, - 47, - 45, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)), - ), - }, - }, - { - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "message": "Add content\n", - "author": { - "name": "Vincent Jacques", - "email": "vincent@vincent-jacques.net", - "date": datetime.datetime( - 2026, - 3, - 5, - 12, - 15, - 50, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)), - ), - }, - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": "7497e018d01503b6abc3053b7896266115e631f6", - "short_id": "7497e018", - "created_at": "2026-03-05T12:15:50.000+01:00", - "parent_ids": ["6d8ca33dae268d3c5835e721e5702ef9dcb43c8c"], - "title": "Add content", - "message": "Add content\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-03-05T12:15:50.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-03-05T12:15:50.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/7497e018d01503b6abc3053b7896266115e631f6", - }, - { - "id": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "short_id": "6d8ca33d", - "created_at": "2026-02-26T09:47:45.000+01:00", - "parent_ids": ["0941ee0a9eac9914cfddf5adec7a9558a2f1c447"], - "title": "Add blah", - "message": "Add blah\n", - "author_name": "Vincent Jacques", - "author_email": "vincent@vincent-jacques.net", - "authored_date": "2026-02-26T09:47:45.000+01:00", - "committer_name": "Vincent Jacques", - "committer_email": "vincent@vincent-jacques.net", - "committed_date": "2026-02-26T09:47:45.000+01:00", - "trailers": {}, - "extended_trailers": {}, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/commit/6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_review_comment_file, - provider_args={ - "pull_request_id": "1", - "commit_id": "7497e018d01503b6abc3053b7896266115e631f6", - "body": "A review comment, on a file, made by the API on 2026-03-11 11:06:19.945026.", - "path": "BLAH.md", - "side": "RIGHT", - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_request_versions", - client_args=("79787061", "1"), - client_kwds={}, - client_return_value=[ - { - "id": 1692137080, - "head_commit_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "base_commit_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_commit_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "created_at": "2026-03-05T11:16:06.468Z", - "merge_request_id": 459277081, - "state": "collected", - "real_size": "1", - "patch_id_sha": "7e2de654ed21ae09a78ad51592379d8a582e90ff", - }, - { - "id": 1682341969, - "head_commit_sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "base_commit_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_commit_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "created_at": "2026-02-26T08:48:23.979Z", - "merge_request_id": 459277081, - "state": "collected", - "real_size": "1", - "patch_id_sha": "f86adf0229d6ac6d77ac8d2087cd5fae3cc052d8", - }, - ], - ), - ClientForwardedCall( - client_method="create_merge_request_discussion", - client_args=( - "79787061", - "1", - { - "body": "A review comment, on a file, made by the API on 2026-03-11 11:06:19.945026.", - "position": { - "position_type": "file", - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "new_path": "BLAH.md", - "old_path": "BLAH.md", - }, - }, - ), - client_kwds={}, - client_return_value={ - "id": "c4604a0d82de5427ec0cdc8780c8f810ea9bec86", - "individual_note": False, - "notes": [ - { - "id": 3149948866, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 11:06:19.945026.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:06:21.007Z", - "updated_at": "2026-03-11T11:06:21.007Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": False, - "resolved_by": None, - "resolved_at": None, - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - } - ], - }, - ), - ], - provider_return_value={ - "data": { - "id": "c4604a0d82de5427ec0cdc8780c8f810ea9bec86:3149948866", - "html_url": None, - "path": "BLAH.md", - "body": "A review comment, on a file, made by the API on 2026-03-11 11:06:19.945026.", - }, - "type": "gitlab", - "raw": { - "data": { - "id": "c4604a0d82de5427ec0cdc8780c8f810ea9bec86", - "individual_note": False, - "notes": [ - { - "id": 3149948866, - "type": "DiffNote", - "body": "A review comment, on a file, made by the API on 2026-03-11 11:06:19.945026.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:06:21.007Z", - "updated_at": "2026-03-11T11:06:21.007Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": False, - "resolved_by": None, - "resolved_at": None, - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - } - ], - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.create_review_comment_reply, - provider_args={ - "pull_request_id": "1", - "body": "A reply to the previous comment, made by the API on 2026-03-11 11:06:21.487947.", - "comment_id": "c4604a0d82de5427ec0cdc8780c8f810ea9bec86:3149948866", - }, - client_calls=[ - ClientForwardedCall( - client_method="create_merge_request_discussion_note", - client_args=( - "79787061", - "1", - "c4604a0d82de5427ec0cdc8780c8f810ea9bec86", - { - "body": "A reply to the previous comment, made by the API on 2026-03-11 11:06:21.487947." - }, - ), - client_kwds={}, - client_return_value={ - "id": 3149949479, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 11:06:21.487947.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:06:31.033Z", - "updated_at": "2026-03-11T11:06:31.033Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": False, - "resolved_by": None, - "resolved_at": None, - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - ), - ], - provider_return_value={ - "data": { - "id": "c4604a0d82de5427ec0cdc8780c8f810ea9bec86:3149949479", - "html_url": None, - "path": "BLAH.md", - "body": "A reply to the previous comment, made by the API on 2026-03-11 11:06:21.487947.", - }, - "type": "gitlab", - "raw": { - "data": { - "id": 3149949479, - "type": "DiffNote", - "body": "A reply to the previous comment, made by the API on 2026-03-11 11:06:21.487947.", - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "created_at": "2026-03-11T11:06:31.033Z", - "updated_at": "2026-03-11T11:06:31.033Z", - "system": False, - "noteable_id": 459277081, - "noteable_type": "MergeRequest", - "project_id": 79787061, - "commit_id": None, - "position": { - "base_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "start_sha": "0941ee0a9eac9914cfddf5adec7a9558a2f1c447", - "head_sha": "7497e018d01503b6abc3053b7896266115e631f6", - "old_path": "BLAH.md", - "new_path": "BLAH.md", - "position_type": "file", - }, - "resolvable": True, - "resolved": False, - "resolved_by": None, - "resolved_at": None, - "suggestions": [], - "confidential": False, - "internal": False, - "imported": False, - "imported_from": "none", - "noteable_iid": 1, - "commands_changes": {}, - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_pull_requests, - provider_args={ - "state": "closed", - "head": None, - "pagination": None, - "request_options": None, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_merge_requests", - client_args=("79787061",), - client_kwds={"state": "closed"}, - client_return_value=[ - { - "id": 464365398, - "iid": 28, - "project_id": 79787061, - "title": "Add content", - "description": "", - "state": "closed", - "created_at": "2026-03-16T10:53:49.098Z", - "updated_at": "2026-03-16T10:53:59.913Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-16T10:53:59.932Z", - "target_branch": "develop", - "source_branch": "topics/blah", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "7497e018d01503b6abc3053b7896266115e631f6", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-16T10:53:52.393Z", - "reference": "!28", - "references": { - "short": "!28", - "relative": "!28", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!28", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/28", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 463099183, - "iid": 26, - "project_id": 79787061, - "title": "PR from API 2026-03-11 11:01:25.358068", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T11:01:25.811Z", - "updated_at": "2026-03-11T11:01:36.336Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T11:01:36.166Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T11:01:27.014Z", - "reference": "!26", - "references": { - "short": "!26", - "relative": "!26", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!26", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/26", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 463096090, - "iid": 25, - "project_id": 79787061, - "title": "PR from API 2026-03-11 10:58:22.520411", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T10:58:32.070Z", - "updated_at": "2026-03-11T10:58:43.367Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T10:58:42.869Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T10:58:33.987Z", - "reference": "!25", - "references": { - "short": "!25", - "relative": "!25", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!25", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/25", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 463089107, - "iid": 24, - "project_id": 79787061, - "title": "PR from API 2026-03-11 10:49:57.363449", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T10:49:57.806Z", - "updated_at": "2026-03-11T10:50:10.729Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T10:50:09.035Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T10:50:00.900Z", - "reference": "!24", - "references": { - "short": "!24", - "relative": "!24", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!24", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/24", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 463078882, - "iid": 23, - "project_id": 79787061, - "title": "PR from API 2026-03-11 10:37:22.100536", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T10:37:24.597Z", - "updated_at": "2026-03-11T10:37:35.394Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T10:37:35.152Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T10:37:26.681Z", - "reference": "!23", - "references": { - "short": "!23", - "relative": "!23", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!23", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/23", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 463071000, - "iid": 22, - "project_id": 79787061, - "title": "PR from API 2026-03-11 10:21:38.194499", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T10:21:38.508Z", - "updated_at": "2026-03-11T10:21:49.127Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T10:21:48.937Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T10:21:41.557Z", - "reference": "!22", - "references": { - "short": "!22", - "relative": "!22", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!22", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/22", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462969620, - "iid": 21, - "project_id": 79787061, - "title": "PR from API 2026-03-11 06:12:27.934693", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T06:12:34.169Z", - "updated_at": "2026-03-11T06:12:44.713Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T06:12:44.410Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T06:12:35.278Z", - "reference": "!21", - "references": { - "short": "!21", - "relative": "!21", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!21", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/21", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462965759, - "iid": 20, - "project_id": 79787061, - "title": "PR from API 2026-03-11 05:56:04.041671", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-11T05:56:13.315Z", - "updated_at": "2026-03-11T05:56:24.119Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-11T05:56:23.900Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-11T05:56:14.470Z", - "reference": "!20", - "references": { - "short": "!20", - "relative": "!20", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!20", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/20", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462566532, - "iid": 19, - "project_id": 79787061, - "title": "PR from API 2026-03-10 11:13:19.494685", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T11:13:20.103Z", - "updated_at": "2026-03-10T11:13:21.993Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T11:13:21.768Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T11:13:21.441Z", - "reference": "!19", - "references": { - "short": "!19", - "relative": "!19", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!19", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/19", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462557344, - "iid": 18, - "project_id": 79787061, - "title": "PR from API 2026-03-10 10:49:11.788318", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T10:49:12.147Z", - "updated_at": "2026-03-10T10:49:35.427Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T10:49:13.376Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T10:49:35.357Z", - "reference": "!18", - "references": { - "short": "!18", - "relative": "!18", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!18", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/18", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462539469, - "iid": 17, - "project_id": 79787061, - "title": "PR from API 2026-03-10 10:09:54.035200", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T10:09:54.436Z", - "updated_at": "2026-03-10T10:09:55.823Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T10:09:55.666Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T10:09:55.672Z", - "reference": "!17", - "references": { - "short": "!17", - "relative": "!17", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!17", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/17", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462534624, - "iid": 16, - "project_id": 79787061, - "title": "PR from API 2026-03-10 09:56:06.472486", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T09:56:06.793Z", - "updated_at": "2026-03-10T09:56:08.046Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T09:56:07.936Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T09:56:07.913Z", - "reference": "!16", - "references": { - "short": "!16", - "relative": "!16", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!16", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/16", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462531581, - "iid": 15, - "project_id": 79787061, - "title": "PR from API 2026-03-10 09:48:02.989404", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T09:48:03.578Z", - "updated_at": "2026-03-10T09:48:05.307Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T09:48:05.112Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T09:48:04.644Z", - "reference": "!15", - "references": { - "short": "!15", - "relative": "!15", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!15", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/15", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462530057, - "iid": 14, - "project_id": 79787061, - "title": "PR from API 2026-03-10 09:43:41.515078", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T09:43:41.799Z", - "updated_at": "2026-03-10T09:43:59.546Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T09:43:43.123Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T09:43:59.532Z", - "reference": "!14", - "references": { - "short": "!14", - "relative": "!14", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!14", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/14", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 462527944, - "iid": 13, - "project_id": 79787061, - "title": "PR from API 2026-03-10 09:37:52.284001", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-10T09:37:52.752Z", - "updated_at": "2026-03-10T09:37:54.846Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-10T09:37:54.567Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-10T09:37:53.944Z", - "reference": "!13", - "references": { - "short": "!13", - "relative": "!13", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!13", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/13", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 461392220, - "iid": 12, - "project_id": 79787061, - "title": "PR from API 2026-03-05 15:12:57.739764", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-05T15:12:58.810Z", - "updated_at": "2026-03-05T15:13:03.726Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-05T15:13:00.192Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-05T15:13:03.716Z", - "reference": "!12", - "references": { - "short": "!12", - "relative": "!12", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!12", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/12", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 461386555, - "iid": 11, - "project_id": 79787061, - "title": "PR from API 2026-03-05 14:56:40.210816", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-05T14:56:40.718Z", - "updated_at": "2026-03-05T14:56:42.166Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-05T14:56:41.894Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-05T14:56:41.989Z", - "reference": "!11", - "references": { - "short": "!11", - "relative": "!11", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!11", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/11", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 461057882, - "iid": 10, - "project_id": 79787061, - "title": "PR from API 2026-03-04 16:11:19.189390", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-04T16:11:20.146Z", - "updated_at": "2026-03-04T16:11:22.235Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-04T16:11:21.969Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-04T16:11:21.356Z", - "reference": "!10", - "references": { - "short": "!10", - "relative": "!10", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!10", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/10", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 461048920, - "iid": 9, - "project_id": 79787061, - "title": "PR from API 2026-03-04 15:44:20.040165", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-04T15:44:20.475Z", - "updated_at": "2026-03-04T15:44:22.256Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-04T15:44:21.587Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-04T15:44:22.240Z", - "reference": "!9", - "references": { - "short": "!9", - "relative": "!9", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!9", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/9", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 460649223, - "iid": 8, - "project_id": 79787061, - "title": "PR from API 2026-03-03 16:11:54.184762", - "description": "Another PR, made through the API.", - "state": "closed", - "created_at": "2026-03-03T16:11:54.845Z", - "updated_at": "2026-03-03T16:11:56.901Z", - "merged_by": None, - "merge_user": None, - "merged_at": None, - "closed_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "closed_at": "2026-03-03T16:11:56.637Z", - "target_branch": "main", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": None, - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-03T16:11:56.264Z", - "reference": "!8", - "references": { - "short": "!8", - "relative": "!8", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!8", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/8", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - ], - ), - ClientForwardedCall( - client_method="get_merge_requests", - client_args=("79787061",), - client_kwds={"state": "merged"}, - client_return_value=[ - { - "id": 464363971, - "iid": 27, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "merged", - "created_at": "2026-03-16T10:51:17.718Z", - "updated_at": "2026-03-16T10:52:00.413Z", - "merged_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "merge_user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "merged_at": "2026-03-16T10:52:00.510Z", - "closed_by": None, - "closed_at": None, - "target_branch": "develop", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": "539e15b113b9a05a9c92ee255f222a4e228fcc41", - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-16T10:51:24.155Z", - "reference": "!27", - "references": { - "short": "!27", - "relative": "!27", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!27", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/27", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - } - ], - ), - ], - provider_return_value={ - "data": [ - { - "id": "464363971", - "number": "27", - "title": "Add blah", - "body": None, - "state": "closed", - "base": {"ref": "develop", "sha": None}, - "head": { - "ref": "topics/blih", - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - "merged": True, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/27", - }, - { - "id": "464363971", - "number": "27", - "title": "Add blah", - "body": None, - "state": "closed", - "base": {"ref": "develop", "sha": None}, - "head": { - "ref": "topics/blih", - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - }, - "merged": True, - "html_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/27", - }, - ], - "type": "gitlab", - "raw": { - "data": [ - { - "id": 464363971, - "iid": 27, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "merged", - "created_at": "2026-03-16T10:51:17.718Z", - "updated_at": "2026-03-16T10:52:00.413Z", - "merged_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "merge_user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "merged_at": "2026-03-16T10:52:00.510Z", - "closed_by": None, - "closed_at": None, - "target_branch": "develop", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": "539e15b113b9a05a9c92ee255f222a4e228fcc41", - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-16T10:51:24.155Z", - "reference": "!27", - "references": { - "short": "!27", - "relative": "!27", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!27", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/27", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - { - "id": 464363971, - "iid": 27, - "project_id": 79787061, - "title": "Add blah", - "description": "", - "state": "merged", - "created_at": "2026-03-16T10:51:17.718Z", - "updated_at": "2026-03-16T10:52:00.413Z", - "merged_by": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "merge_user": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "merged_at": "2026-03-16T10:52:00.510Z", - "closed_by": None, - "closed_at": None, - "target_branch": "develop", - "source_branch": "topics/blih", - "user_notes_count": 0, - "upvotes": 0, - "downvotes": 0, - "author": { - "id": 150871, - "username": "jacquev6", - "public_email": "", - "name": "Vincent Jacques", - "state": "active", - "locked": False, - "avatar_url": "https://secure.gravatar.com/avatar/64b276c831d40984ef60c18f9d8d046cffc9b20292e357df709b929a4dc3c188?s=80&d=identicon", - "web_url": "https://gitlab.com/jacquev6", - }, - "assignees": [], - "assignee": None, - "reviewers": [], - "source_project_id": 79787061, - "target_project_id": 79787061, - "labels": [], - "draft": False, - "imported": False, - "imported_from": "none", - "work_in_progress": False, - "milestone": None, - "merge_when_pipeline_succeeds": False, - "merge_status": "can_be_merged", - "detailed_merge_status": "not_open", - "merge_after": None, - "sha": "6d8ca33dae268d3c5835e721e5702ef9dcb43c8c", - "merge_commit_sha": "539e15b113b9a05a9c92ee255f222a4e228fcc41", - "squash_commit_sha": None, - "discussion_locked": None, - "should_remove_source_branch": None, - "force_remove_source_branch": True, - "prepared_at": "2026-03-16T10:51:24.155Z", - "reference": "!27", - "references": { - "short": "!27", - "relative": "!27", - "full": "jacquev6-sentry/test-sentry-integration-dev-jacquev6!27", - }, - "web_url": "https://gitlab.com/jacquev6-sentry/test-sentry-integration-dev-jacquev6/-/merge_requests/27", - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": None, - "human_total_time_spent": None, - }, - "squash": False, - "squash_on_merge": False, - "task_completion_status": {"count": 0, "completed_count": 0}, - "has_conflicts": False, - "blocking_discussions_resolved": True, - "approvals_before_merge": None, - }, - ], - "headers": None, - }, - "meta": {"next_cursor": None}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_git_commit, - provider_args={"sha": "6104942438c14ec7bd21c6cd5bd995272b3faff6"}, - client_calls=[ - ClientForwardedCall( - client_method="get_commit", - client_args=( - "79787061", - "6104942438c14ec7bd21c6cd5bd995272b3faff6", - ), - client_kwds={}, - client_return_value={ - "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", - "short_id": "6104942438c", - "title": "Sanitize for network graph", - "message": "Sanitize for network graph", - "author_name": "randx", - "author_email": "user@example.com", - "created_at": "2021-09-20T09:06:12.300+03:00", - }, - ), - ], - provider_return_value={ - "data": { - "sha": "6104942438c14ec7bd21c6cd5bd995272b3faff6", - "tree": {"sha": "6104942438c14ec7bd21c6cd5bd995272b3faff6"}, - "message": "Sanitize for network graph", - }, - "type": "gitlab", - "raw": { - "data": { - "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", - "short_id": "6104942438c", - "title": "Sanitize for network graph", - "message": "Sanitize for network graph", - "author_name": "randx", - "author_email": "user@example.com", - "created_at": "2021-09-20T09:06:12.300+03:00", - }, - "headers": None, - }, - "meta": {}, - }, - ), - ForwardToClientTest( - provider_method=GitLabProvider.get_tree, - provider_args={ - "tree_sha": "6104942438c14ec7bd21c6cd5bd995272b3faff6", - "recursive": True, - }, - client_calls=[ - ClientForwardedCall( - client_method="get_repository_tree", - client_args=("79787061",), - client_kwds={ - "ref": "6104942438c14ec7bd21c6cd5bd995272b3faff6", - "recursive": True, - }, - client_return_value=[ - { - "id": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba", - "name": "html", - "type": "tree", - "path": "files/html", - "mode": "040000", - }, - { - "id": "4535904260b1082e14f867f7a24fd8c21495bde3", - "name": "images", - "type": "tree", - "path": "files/images", - "mode": "040000", - }, - ], - ), - ], - provider_return_value={ - "data": { - "sha": "6104942438c14ec7bd21c6cd5bd995272b3faff6", - "tree": [ - { - "path": "files/html", - "mode": "040000", - "type": "tree", - "sha": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba", - "size": None, - }, - { - "path": "files/images", - "mode": "040000", - "type": "tree", - "sha": "4535904260b1082e14f867f7a24fd8c21495bde3", - "size": None, - }, - ], - "truncated": False, - }, - "type": "gitlab", - "raw": { - "data": [ - { - "id": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba", - "name": "html", - "type": "tree", - "path": "files/html", - "mode": "040000", - }, - { - "id": "4535904260b1082e14f867f7a24fd8c21495bde3", - "name": "images", - "type": "tree", - "path": "files/images", - "mode": "040000", - }, - ], - "headers": None, - }, - "meta": {}, - }, - ), - ], - ids=lambda param: param.provider_method.__name__, -) -def test_forward_to_client(client, provider: GitLabProvider, param: ForwardToClientTest): - # Setup client mock return values - for client_call in param.client_calls: - getattr(client, client_call.client_method).return_value = client_call.client_return_value - - # Call the provider, check the return value - assert param.provider_method(provider, **param.provider_args) == param.provider_return_value - - # Check client calls (method and args) - assert len(client.mock_calls) == len(param.client_calls) - for client_call, mock_call in zip(param.client_calls, client.mock_calls): - assert mock_call[0] == client_call.client_method - assert mock_call[1] == client_call.client_args - assert mock_call[2] == client_call.client_kwds - - -class TestGetArchiveLink: - def test_returns_tar_gz_url(self, provider: GitLabProvider, client: unittest.mock.MagicMock): - client.base_url = "https://gitlab.example.com" - client.get_access_token.return_value = { - "access_token": "fake-gitlab-token", - "permissions": None, - } - - result = provider.get_archive_link("main") - - assert result["type"] == "gitlab" - assert result["data"]["url"] == ( - "https://gitlab.example.com/api/v4/projects/79787061/repository/archive.tar.gz?sha=main" - ) - assert result["data"]["headers"] == {"Authorization": "Bearer fake-gitlab-token"} - - def test_returns_zip_url(self, provider: GitLabProvider, client: unittest.mock.MagicMock): - client.base_url = "https://gitlab.example.com" - client.get_access_token.return_value = { - "access_token": "fake-gitlab-token", - "permissions": None, - } - - result = provider.get_archive_link("main", "zip") - - assert result["type"] == "gitlab" - assert result["data"]["url"] == ( - "https://gitlab.example.com/api/v4/projects/79787061/repository/archive.zip?sha=main" - ) - assert result["data"]["headers"] == {"Authorization": "Bearer fake-gitlab-token"} - - def test_empty_ref_omits_sha_param( - self, provider: GitLabProvider, client: unittest.mock.MagicMock - ): - client.base_url = "https://gitlab.example.com" - client.get_access_token.return_value = { - "access_token": "fake-gitlab-token", - "permissions": None, - } - - result = provider.get_archive_link("") - - assert "?sha=" not in result["data"]["url"] diff --git a/tests/sentry/scm/unit/test_helpers.py b/tests/sentry/scm/unit/test_helpers.py deleted file mode 100644 index 7c53d9af8ae448..00000000000000 --- a/tests/sentry/scm/unit/test_helpers.py +++ /dev/null @@ -1,22 +0,0 @@ -from sentry.scm.private.helpers import exec_provider_fn -from tests.sentry.scm.test_fixtures import BaseTestProvider - - -def test_exec_provider_fn() -> None: - metrics = [] - - def record_count(k, a, t): - metrics.append((k, a, t)) - - provider = BaseTestProvider() - result = exec_provider_fn( - provider, - referrer="emerge", - provider_fn=lambda: 42, - record_count=record_count, - ) - assert result == 42 - assert metrics == [ - ("sentry.scm.actions.success_by_provider", 1, {"provider": "BaseTestProvider"}), - ("sentry.scm.actions.success_by_referrer", 1, {"referrer": "emerge"}), - ] diff --git a/tests/sentry/scm/unit/test_scm_actions.py b/tests/sentry/scm/unit/test_scm_actions.py deleted file mode 100644 index 3998a3dfaa843b..00000000000000 --- a/tests/sentry/scm/unit/test_scm_actions.py +++ /dev/null @@ -1,856 +0,0 @@ -from collections.abc import Callable -from contextlib import contextmanager -from typing import Any - -import pytest - -from sentry.scm.actions import ( - SourceCodeManager, - compare_commits, - create_branch, - create_check_run, - create_git_blob, - create_git_commit, - create_git_tree, - create_issue_comment, - create_issue_comment_reaction, - create_issue_reaction, - create_pull_request, - create_pull_request_comment, - create_pull_request_comment_reaction, - create_pull_request_draft, - create_pull_request_reaction, - create_review, - create_review_comment_file, - create_review_comment_reply, - delete_issue_comment, - delete_issue_comment_reaction, - delete_issue_reaction, - delete_pull_request_comment, - delete_pull_request_comment_reaction, - delete_pull_request_reaction, - get_branch, - get_capabilities, - get_check_run, - get_commit, - get_commits, - get_commits_by_path, - get_file_content, - get_git_commit, - get_issue_comment_reactions, - get_issue_comments, - get_issue_reactions, - get_pull_request, - get_pull_request_comment_reactions, - get_pull_request_comments, - get_pull_request_commits, - get_pull_request_diff, - get_pull_request_files, - get_pull_request_reactions, - get_pull_requests, - get_tree, - minimize_comment, - request_review, - update_branch, - update_check_run, - update_pull_request, -) -from sentry.scm.errors import ( - SCMCodedError, - SCMProviderException, - SCMUnhandledException, -) -from sentry.scm.types import ( - GetBranchProtocol, - GetIssueReactionsProtocol, - PaginatedActionResult, - ReactionResult, - Referrer, - Repository, -) -from tests.sentry.scm.test_fixtures import BaseTestProvider - - -@contextmanager -def raises_with_code(exc_class, code): - with pytest.raises(exc_class) as exc_info: - yield exc_info - assert exc_info.value.code == code, f"Expected code {code!r}, got {exc_info.value.code!r}" - - -def fetch_repository(oid, rid) -> Repository: - return { - "integration_id": 1, - "name": "test", - "organization_id": 1, - "is_active": True, - "external_id": None, - } - - -ALL_ACTIONS: tuple[tuple[Callable[..., Any], dict[str, Any]], ...] = ( - # Issue comments - (get_issue_comments, {"issue_id": "1"}), - (create_issue_comment, {"issue_id": "1", "body": "test"}), - (delete_issue_comment, {"issue_id": "1", "comment_id": "1"}), - # Pull request - (get_pull_request, {"pull_request_id": "1"}), - # Pull request comments - (get_pull_request_comments, {"pull_request_id": "1"}), - (create_pull_request_comment, {"pull_request_id": "1", "body": "test"}), - (delete_pull_request_comment, {"pull_request_id": "1", "comment_id": "1"}), - # Issue comment reactions - (get_issue_comment_reactions, {"issue_id": "1", "comment_id": "1"}), - (create_issue_comment_reaction, {"issue_id": "1", "comment_id": "1", "reaction": "eyes"}), - (delete_issue_comment_reaction, {"issue_id": "1", "comment_id": "1", "reaction_id": "123"}), - # Pull request comment reactions - (get_pull_request_comment_reactions, {"pull_request_id": "1", "comment_id": "1"}), - ( - create_pull_request_comment_reaction, - {"pull_request_id": "1", "comment_id": "1", "reaction": "eyes"}, - ), - ( - delete_pull_request_comment_reaction, - {"pull_request_id": "1", "comment_id": "1", "reaction_id": "123"}, - ), - # Issue reactions - (get_issue_reactions, {"issue_id": "1"}), - (create_issue_reaction, {"issue_id": "1", "reaction": "eyes"}), - (delete_issue_reaction, {"issue_id": "1", "reaction_id": "456"}), - # Pull request reactions - (get_pull_request_reactions, {"pull_request_id": "1"}), - (create_pull_request_reaction, {"pull_request_id": "1", "reaction": "eyes"}), - (delete_pull_request_reaction, {"pull_request_id": "1", "reaction_id": "456"}), - # Branch operations - (get_branch, {"branch": "main"}), - (create_branch, {"branch": "feature", "sha": "abc123"}), - (update_branch, {"branch": "feature", "sha": "def456"}), - # Git blob operations - (create_git_blob, {"content": "hello", "encoding": "utf-8"}), - # File content operations - (get_file_content, {"path": "README.md"}), - # Commit operations - (get_commit, {"sha": "abc123"}), - (get_commits, {}), - (get_commits_by_path, {"path": "src/main.py"}), - (compare_commits, {"start_sha": "aaa", "end_sha": "bbb"}), - # Git data operations - (get_tree, {"tree_sha": "tree123"}), - (get_git_commit, {"sha": "abc123"}), - (create_git_tree, {"tree": [{"path": "f.py", "mode": "100644", "type": "blob", "sha": "x"}]}), - (create_git_commit, {"message": "msg", "tree_sha": "t", "parent_shas": ["p"]}), - # Expanded pull request operations - (get_pull_request_files, {"pull_request_id": "1"}), - (get_pull_request_commits, {"pull_request_id": "1"}), - (get_pull_request_diff, {"pull_request_id": "1"}), - (get_pull_requests, {}), - (create_pull_request, {"title": "T", "body": "B", "head": "h", "base": "b"}), - (create_pull_request_draft, {"title": "T", "body": "B", "head": "h", "base": "b"}), - (update_pull_request, {"pull_request_id": "1"}), - (request_review, {"pull_request_id": "1", "reviewers": ["user1"]}), - # Review operations - ( - create_review_comment_file, - { - "pull_request_id": "1", - "commit_id": "abc", - "body": "comment", - "path": "f.py", - "side": "RIGHT", - }, - ), - ( - create_review_comment_reply, - { - "pull_request_id": "1", - "body": "comment", - "comment_id": "123", - }, - ), - ( - create_review, - { - "pull_request_id": "1", - "commit_sha": "abc", - "event": "comment", - "comments": [], - }, - ), - # Check run operations - (create_check_run, {"name": "check", "head_sha": "abc"}), - (get_check_run, {"check_run_id": "300"}), - (update_check_run, {"check_run_id": "300"}), - # GraphQL mutation operations - (minimize_comment, {"comment_node_id": "IC_abc", "reason": "OUTDATED"}), -) - - -@pytest.mark.parametrize(("action", "kwargs"), ALL_ACTIONS) -def test_rate_limited_action(action: Callable[..., Any], kwargs: dict[str, Any]): - class RateLimitedProvider(BaseTestProvider): - def is_rate_limited(self, referrer): - return True - - scm = SourceCodeManager(RateLimitedProvider()) - - with raises_with_code(SCMCodedError, "rate_limit_exceeded"): - action(scm, **kwargs) - - -def test_scm_is_instance_of_scm() -> None: - # This weird test is justified by the creation of the dynamic Facade subclass in SourceCodeManager.__new__. - # In a previous version, it was returning a subclass of Facade, but not of SourceCodeManager. - provider = BaseTestProvider() - scm = SourceCodeManager(provider) - assert isinstance(scm, SourceCodeManager) - assert scm.provider is provider - - -def test_repository_not_found() -> None: - with raises_with_code(SCMCodedError, "repository_not_found"): - SourceCodeManager.make_from_repository_id( - organization_id=1, - repository_id=1, - fetch_repository=lambda _a, _b: None, - ) - - -def test_repository_inactive() -> None: - with raises_with_code(SCMCodedError, "repository_inactive"): - SourceCodeManager.make_from_repository_id( - organization_id=1, - repository_id=1, - fetch_repository=lambda _a, _b: { - "integration_id": 1, - "name": "test", - "organization_id": 1, - "is_active": False, - "external_id": None, - }, - ) - - -def test_repository_organization_mismatch() -> None: - with raises_with_code(SCMCodedError, "repository_organization_mismatch"): - SourceCodeManager.make_from_repository_id( - organization_id=2, - repository_id=1, - fetch_repository=fetch_repository, - ) - - -def make_scm(): - return SourceCodeManager(BaseTestProvider()) - - -def _check_issue_comments(result: Any) -> None: - assert len(result["data"]) == 1 - assert result["data"][0]["id"] == "101" - assert result["data"][0]["body"] == "Test comment" - assert result["data"][0]["author"]["username"] == "testuser" - assert result["type"] == "github" - - -def _check_pull_request(result: Any) -> None: - pr = result["data"] - assert pr["id"] == "42" - assert pr["number"] == 1 - assert pr["title"] == "Test PR" - assert pr["head"]["sha"] == "abc123" - assert pr["head"]["ref"] == "feature-branch" - assert pr["base"]["sha"] == "def456" - assert result["type"] == "github" - - -def _check_pull_request_comments(result: Any) -> None: - assert len(result["data"]) == 1 - assert result["data"][0]["id"] == "201" - assert result["data"][0]["body"] == "PR review comment" - assert result["data"][0]["author"]["username"] == "reviewer" - assert result["type"] == "github" - - -def _check_comment_reactions(result: Any) -> None: - assert len(result["data"]) == 2 - assert result["data"][0]["id"] == "1" - assert result["data"][0]["content"] == "+1" - assert result["data"][1]["id"] == "2" - assert result["data"][1]["content"] == "eyes" - assert result["type"] == "github" - - -def _check_pr_comment_reactions(result: Any) -> None: - assert len(result["data"]) == 2 - assert result["data"][0]["id"] == "3" - assert result["data"][0]["content"] == "rocket" - assert result["data"][1]["id"] == "4" - assert result["data"][1]["content"] == "hooray" - assert result["type"] == "github" - - -def _check_issue_reactions(result: Any) -> None: - assert len(result["data"]) == 2 - assert result["data"][0]["id"] == "1" - assert result["data"][0]["content"] == "+1" - assert result["data"][0]["author"]["username"] == "testuser" - assert result["data"][1]["id"] == "2" - assert result["data"][1]["content"] == "heart" - assert result["data"][1]["author"]["username"] == "otheruser" - assert result["type"] == "github" - - -def _check_pr_reactions(result: Any) -> None: - assert len(result["data"]) == 2 - assert result["data"][0]["id"] == "5" - assert result["data"][0]["content"] == "laugh" - assert result["data"][0]["author"]["username"] == "testuser" - assert result["data"][1]["id"] == "6" - assert result["data"][1]["content"] == "confused" - assert result["data"][1]["author"]["username"] == "otheruser" - assert result["type"] == "github" - - -def _check_get_branch(result: Any) -> None: - assert result["data"]["ref"] == "refs/heads/main" - assert result["data"]["sha"] == "abc123def456" - assert result["type"] == "github" - - -def _check_create_branch(result: Any) -> None: - assert result["data"]["ref"] == "feature" - assert result["data"]["sha"] == "abc123" - assert result["type"] == "github" - - -def _check_update_branch(result: Any) -> None: - assert result["data"]["ref"] == "feature" - assert result["data"]["sha"] == "def456" - assert result["type"] == "github" - - -def _check_create_git_blob(result: Any) -> None: - assert result["data"]["sha"] == "blob123abc" - assert result["type"] == "github" - - -def _check_file_content(result: Any) -> None: - fc = result["data"] - assert fc["path"] == "README.md" - assert fc["content"] == "SGVsbG8gV29ybGQ=" - assert fc["encoding"] == "base64" - assert result["type"] == "github" - - -def _check_get_commit(result: Any) -> None: - c = result["data"] - assert c["id"] == "abc123" - assert c["message"] == "Fix bug" - assert c["author"]["name"] == "Test User" - assert result["type"] == "github" - - -def _check_get_commits(result: Any) -> None: - assert len(result["data"]) == 1 - assert result["data"][0]["id"] == "abc123" - assert result["type"] == "github" - - -def _check_compare_commits(result: Any) -> None: - assert len(result["data"]) == 1 - c = result["data"][0] - assert c["id"] == "abc123" - assert c["message"] == "Fix bug" - assert result["type"] == "github" - - -def _check_get_tree(result: Any) -> None: - gt = result["data"] - assert len(gt["tree"]) == 1 - assert gt["tree"][0]["path"] == "src/main.py" - assert gt["truncated"] is False - assert result["type"] == "github" - - -def _check_get_git_commit(result: Any) -> None: - gc = result["data"] - assert gc["sha"] == "abc123" - assert gc["tree"]["sha"] == "tree456" - assert result["type"] == "github" - - -def _check_create_git_tree(result: Any) -> None: - gt = result["data"] - assert len(gt["tree"]) == 1 - assert result["type"] == "github" - - -def _check_create_git_commit(result: Any) -> None: - gc = result["data"] - assert gc["sha"] == "newcommit123" - assert gc["message"] == "msg" - assert result["type"] == "github" - - -def _check_pr_files(result: Any) -> None: - assert len(result["data"]) == 1 - assert result["data"][0]["filename"] == "src/main.py" - assert result["type"] == "github" - - -def _check_pr_commits(result: Any) -> None: - assert len(result["data"]) == 1 - assert result["data"][0]["sha"] == "commit123" - assert result["data"][0]["message"] == "Fix bug" - assert result["type"] == "github" - - -def _check_pr_diff(result: Any) -> None: - assert "diff --git" in result["data"] - assert result["type"] == "github" - - -def _check_list_pull_requests(result: Any) -> None: - assert len(result["data"]) == 1 - assert result["data"][0]["number"] == 1 - assert result["type"] == "github" - - -def _check_create_pull_request(result: Any) -> None: - pr = result["data"] - assert pr["title"] == "T" - assert pr["body"] == "B" - assert result["type"] == "github" - - -def _check_update_pull_request(result: Any) -> None: - pr = result["data"] - assert pr["title"] == "Test PR" - assert result["type"] == "github" - - -def _check_none(result: Any) -> None: - assert result is None - - -def _check_created_comment(result: Any) -> None: - comment = result["data"] - assert comment["id"] == "101" - assert result["type"] == "github" - - -def _check_created_pr_comment(result: Any) -> None: - comment = result["data"] - assert comment["id"] == "201" - assert result["type"] == "github" - - -def _check_created_reaction(result: Any) -> None: - reaction = result["data"] - assert reaction["id"] == "1" - assert reaction["content"] == "eyes" - assert result["type"] == "github" - - -def _check_review_comment(result: Any) -> None: - rc = result["data"] - assert rc["id"] == "100" - assert rc["body"] == "comment" - assert result["type"] == "github" - - -def _check_review(result: Any) -> None: - r = result["data"] - assert r["id"] == "200" - assert result["type"] == "github" - - -def _check_create_check_run(result: Any) -> None: - cr = result["data"] - assert cr["name"] == "check" - assert result["type"] == "github" - - -def _check_get_check_run(result: Any) -> None: - cr = result["data"] - assert cr["id"] == "300" - assert cr["status"] == "completed" - assert result["type"] == "github" - - -def _check_update_check_run(result: Any) -> None: - cr = result["data"] - assert cr["id"] == "300" - assert result["type"] == "github" - - -ACTION_TESTS: tuple[tuple[Callable[..., Any], dict[str, Any], Callable[..., Any]], ...] = ( - (get_issue_comments, {"issue_id": "1"}, _check_issue_comments), - ( - create_issue_comment, - {"issue_id": "1", "body": "test"}, - _check_created_comment, - ), - (delete_issue_comment, {"issue_id": "1", "comment_id": "1"}, _check_none), - (get_pull_request, {"pull_request_id": "1"}, _check_pull_request), - ( - get_pull_request_comments, - {"pull_request_id": "1"}, - _check_pull_request_comments, - ), - ( - create_pull_request_comment, - {"pull_request_id": "1", "body": "test"}, - _check_created_pr_comment, - ), - ( - delete_pull_request_comment, - {"pull_request_id": "1", "comment_id": "1"}, - _check_none, - ), - ( - get_issue_comment_reactions, - {"issue_id": "1", "comment_id": "1"}, - _check_comment_reactions, - ), - ( - create_issue_comment_reaction, - {"issue_id": "1", "comment_id": "1", "reaction": "eyes"}, - _check_created_reaction, - ), - ( - delete_issue_comment_reaction, - {"issue_id": "1", "comment_id": "1", "reaction_id": "123"}, - _check_none, - ), - ( - get_pull_request_comment_reactions, - {"pull_request_id": "1", "comment_id": "1"}, - _check_pr_comment_reactions, - ), - ( - create_pull_request_comment_reaction, - {"pull_request_id": "1", "comment_id": "1", "reaction": "eyes"}, - _check_created_reaction, - ), - ( - delete_pull_request_comment_reaction, - {"pull_request_id": "1", "comment_id": "1", "reaction_id": "123"}, - _check_none, - ), - (get_issue_reactions, {"issue_id": "1"}, _check_issue_reactions), - ( - create_issue_reaction, - {"issue_id": "1", "reaction": "eyes"}, - _check_created_reaction, - ), - (delete_issue_reaction, {"issue_id": "1", "reaction_id": "456"}, _check_none), - ( - get_pull_request_reactions, - {"pull_request_id": "1"}, - _check_pr_reactions, - ), - ( - create_pull_request_reaction, - {"pull_request_id": "1", "reaction": "eyes"}, - _check_created_reaction, - ), - ( - delete_pull_request_reaction, - {"pull_request_id": "1", "reaction_id": "456"}, - _check_none, - ), - (get_branch, {"branch": "main"}, _check_get_branch), - (create_branch, {"branch": "feature", "sha": "abc123"}, _check_create_branch), - (update_branch, {"branch": "feature", "sha": "def456"}, _check_update_branch), - ( - create_git_blob, - {"content": "hello", "encoding": "utf-8"}, - _check_create_git_blob, - ), - (get_file_content, {"path": "README.md"}, _check_file_content), - (get_commit, {"sha": "abc123"}, _check_get_commit), - (get_commits, {}, _check_get_commits), - (get_commits_by_path, {"path": "src/main.py"}, _check_get_commits), - ( - compare_commits, - {"start_sha": "aaa", "end_sha": "bbb"}, - _check_compare_commits, - ), - (get_tree, {"tree_sha": "tree123"}, _check_get_tree), - (get_git_commit, {"sha": "abc123"}, _check_get_git_commit), - ( - create_git_tree, - {"tree": [{"path": "f.py", "mode": "100644", "type": "blob", "sha": "x"}]}, - _check_create_git_tree, - ), - ( - create_git_commit, - {"message": "msg", "tree_sha": "t", "parent_shas": ["p"]}, - _check_create_git_commit, - ), - (get_pull_request_files, {"pull_request_id": "1"}, _check_pr_files), - (get_pull_request_commits, {"pull_request_id": "1"}, _check_pr_commits), - (get_pull_request_diff, {"pull_request_id": "1"}, _check_pr_diff), - (get_pull_requests, {}, _check_list_pull_requests), - ( - create_pull_request, - {"title": "T", "body": "B", "head": "h", "base": "b"}, - _check_create_pull_request, - ), - ( - create_pull_request_draft, - {"title": "T", "body": "B", "head": "h", "base": "b"}, - _check_create_pull_request, - ), - (update_pull_request, {"pull_request_id": "1"}, _check_update_pull_request), - ( - request_review, - {"pull_request_id": "1", "reviewers": ["user1"]}, - _check_none, - ), - ( - create_review_comment_file, - { - "pull_request_id": "1", - "commit_id": "abc", - "body": "comment", - "path": "f.py", - "side": "RIGHT", - }, - _check_review_comment, - ), - ( - create_review_comment_reply, - { - "pull_request_id": "1", - "body": "comment", - "comment_id": "123", - }, - _check_review_comment, - ), - ( - create_review, - { - "pull_request_id": "1", - "commit_sha": "abc", - "event": "comment", - "comments": [], - }, - _check_review, - ), - ( - create_check_run, - {"name": "check", "head_sha": "abc"}, - _check_create_check_run, - ), - ( - get_check_run, - {"check_run_id": "300"}, - _check_get_check_run, - ), - ( - update_check_run, - {"check_run_id": "300"}, - _check_update_check_run, - ), - ( - minimize_comment, - {"comment_node_id": "IC_abc", "reason": "OUTDATED"}, - _check_none, - ), -) - - -@pytest.mark.parametrize(("method", "kwargs", "check"), ACTION_TESTS) -def test_action_success(method, kwargs: dict[str, Any], check): - metrics = [] - - def record_count(k, a, t): - metrics.append((k, a, t)) - - scm = SourceCodeManager(BaseTestProvider(), record_count=record_count) - check(method(scm, **kwargs)) - - assert metrics == [ - ("sentry.scm.actions.success_by_provider", 1, {"provider": "BaseTestProvider"}), - ("sentry.scm.actions.success_by_referrer", 1, {"referrer": "shared"}), - ] - - -def test_provider_exception_is_not_wrapped() -> None: - """SCMProviderException should pass through exec_provider_fn, not be wrapped as SCMUnhandledException.""" - - class FailingProvider(BaseTestProvider): - def get_issue_reactions( - self, issue_id: str, pagination=None, request_options=None - ) -> PaginatedActionResult[ReactionResult]: - raise SCMProviderException("GitHub API error") - - scm = SourceCodeManager(FailingProvider()) - - with pytest.raises(SCMProviderException): - assert isinstance(scm, GetIssueReactionsProtocol) - scm.get_issue_reactions(issue_id="1") - - -class MinimalProvider: - """A provider that implements Provider but no action protocols.""" - - organization_id: int = 1 - repository: Repository = { - "integration_id": 1, - "name": "test", - "organization_id": 1, - "is_active": True, - "external_id": None, - } - - def is_rate_limited(self, referrer: Referrer) -> bool: - return False - - -@pytest.mark.parametrize(("action", "kwargs"), ALL_ACTIONS) -def test_exec_raises_provider_not_supported_for_all_actions( - action: Callable[..., Any], - kwargs: dict[str, Any], -): - """Every SCM action raises SCMProviderNotSupported when the provider lacks the protocol.""" - scm = SourceCodeManager(MinimalProvider()) - - with pytest.raises(AttributeError): - action(scm, **kwargs) - - -def test_exec_wraps_unhandled_exception() -> None: - """Non-SCM exceptions raised by the provider are wrapped as SCMUnhandledException.""" - - class ExplodingProvider(BaseTestProvider): - def get_branch(self, branch, request_options=None): - raise RuntimeError("unexpected failure") - - scm = SourceCodeManager(ExplodingProvider()) - - with pytest.raises(SCMUnhandledException): - assert isinstance(scm, GetBranchProtocol) - scm.get_branch(branch="main") - - -def test_exec_records_failure_metric_on_unhandled_exception() -> None: - """record_count is called with the failure metric when a non-SCM exception occurs.""" - metrics: list[tuple[str, int, dict[str, str]]] = [] - - class ExplodingProvider(BaseTestProvider): - def get_branch(self, branch, request_options=None): - raise RuntimeError("boom") - - scm = SourceCodeManager( - ExplodingProvider(), record_count=lambda k, a, t: metrics.append((k, a, t)) - ) - - with pytest.raises(SCMUnhandledException): - assert isinstance(scm, GetBranchProtocol) - scm.get_branch(branch="main") - - assert metrics == [ - ("sentry.scm.actions.failed_by_provider", 1, {"provider": ExplodingProvider.__name__}), - ("sentry.scm.actions.failed_by_referrer", 1, {"referrer": "shared"}), - ] - - -def test_exec_passes_custom_referrer() -> None: - """The referrer set on SourceCodeManager is forwarded through _exec to exec_provider_fn.""" - metrics: list[tuple[str, int, dict[str, str]]] = [] - - scm = SourceCodeManager( - BaseTestProvider(), - referrer="autofix", - record_count=lambda k, a, t: metrics.append((k, a, t)), - ) - assert isinstance(scm, GetBranchProtocol) - scm.get_branch(branch="main") - - referrer_metrics = [(k, a, t) for k, a, t in metrics if "referrer" in t] - assert referrer_metrics == [ - ("sentry.scm.actions.success_by_referrer", 1, {"referrer": "autofix"}), - ] - - -def test_exec_passes_custom_record_count() -> None: - """A custom record_count callable provided at construction is used by _exec.""" - calls: list[tuple[str, int, dict[str, str]]] = [] - - def custom_record(key: str, amount: int, tags: dict[str, str]) -> None: - calls.append((key, amount, tags)) - - scm = SourceCodeManager(BaseTestProvider(), record_count=custom_record) - assert isinstance(scm, GetBranchProtocol) - scm.get_branch(branch="main") - - assert len(calls) == 2 - assert calls[0] == ( - "sentry.scm.actions.success_by_provider", - 1, - {"provider": "BaseTestProvider"}, - ) - assert calls[1] == ("sentry.scm.actions.success_by_referrer", 1, {"referrer": "shared"}) - - -def test_get_capabilities() -> None: - assert list(get_capabilities(SourceCodeManager(BaseTestProvider()))) == [ - "CompareCommitsProtocol", - "CreateBranchProtocol", - "CreateCheckRunProtocol", - "CreateGitBlobProtocol", - "CreateGitCommitProtocol", - "CreateGitTreeProtocol", - "CreateIssueCommentProtocol", - "CreateIssueCommentReactionProtocol", - "CreateIssueReactionProtocol", - "CreatePullRequestCommentProtocol", - "CreatePullRequestCommentReactionProtocol", - "CreatePullRequestDraftProtocol", - "CreatePullRequestProtocol", - "CreatePullRequestReactionProtocol", - "CreateReviewCommentFileProtocol", - "CreateReviewCommentReplyProtocol", - "CreateReviewProtocol", - "DeleteIssueCommentProtocol", - "DeleteIssueCommentReactionProtocol", - "DeleteIssueReactionProtocol", - "DeletePullRequestCommentProtocol", - "DeletePullRequestCommentReactionProtocol", - "DeletePullRequestReactionProtocol", - "GetBranchProtocol", - "GetCheckRunProtocol", - "GetCommitProtocol", - "GetCommitsByPathProtocol", - "GetCommitsProtocol", - "GetFileContentProtocol", - "GetGitCommitProtocol", - "GetIssueCommentReactionsProtocol", - "GetIssueCommentsProtocol", - "GetIssueReactionsProtocol", - "GetPullRequestCommentReactionsProtocol", - "GetPullRequestCommentsProtocol", - "GetPullRequestCommitsProtocol", - "GetPullRequestDiffProtocol", - "GetPullRequestFilesProtocol", - "GetPullRequestProtocol", - "GetPullRequestReactionsProtocol", - "GetPullRequestsProtocol", - "GetTreeProtocol", - "MinimizeCommentProtocol", - "RequestReviewProtocol", - "UpdateBranchProtocol", - "UpdateCheckRunProtocol", - "UpdatePullRequestProtocol", - ] - - class IncapableProvider: - organization_id: int - repository: Repository - - def is_rate_limited(self, referrer: Referrer) -> bool: - return False - - assert list(get_capabilities(SourceCodeManager(IncapableProvider()))) == [] From 5f6b103ce29d8421155c1efe211c7f9ca8ec78dd Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 14 Apr 2026 15:07:18 -0500 Subject: [PATCH 3/6] Add new initalization function and reorganize --- pyproject.toml | 1 + src/sentry/scm/endpoints/scm_rpc.py | 9 +- src/sentry/scm/factory.py | 16 +++ src/sentry/scm/private/helpers.py | 92 +++++++------- src/sentry/scm/private/ipc.py | 27 +--- src/sentry/scm/private/stream_producer.py | 2 +- src/sentry/scm/types.py | 4 +- tests/sentry/scm/endpoints/test_scm_rpc.py | 8 +- .../integration/test_helpers_integration.py | 119 +----------------- uv.lock | 14 +++ 10 files changed, 101 insertions(+), 191 deletions(-) create mode 100644 src/sentry/scm/factory.py diff --git a/pyproject.toml b/pyproject.toml index 461777036db9f5..74a73f7ccc07ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ dependencies = [ "sentry-protos>=0.8.10", "sentry-redis-tools>=0.5.0", "sentry-relay>=0.9.25", + "sentry-scm>=0.1.7", "sentry-sdk[http2]>=2.47.0", "sentry-usage-accountant>=0.0.10", # remove once there are no unmarked transitive dependencies on setuptools diff --git a/src/sentry/scm/endpoints/scm_rpc.py b/src/sentry/scm/endpoints/scm_rpc.py index b4d16826f1d46f..a8892c4ca26446 100644 --- a/src/sentry/scm/endpoints/scm_rpc.py +++ b/src/sentry/scm/endpoints/scm_rpc.py @@ -7,14 +7,19 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, internal_cell_silo_endpoint -from sentry.scm.private.helpers import fetch_repository, fetch_service_provider -from sentry.scm.private.ipc import record_count_metric +from sentry.scm.private.helpers import ( + fetch_repository, + fetch_service_provider, + record_count_metric, + report_error_to_sentry, +) server = RpcServer( secrets=settings.SCM_RPC_SHARED_SECRET, fetch_repository=fetch_repository, fetch_provider=fetch_service_provider, record_count=record_count_metric, + emit_error=report_error_to_sentry, ) diff --git a/src/sentry/scm/factory.py b/src/sentry/scm/factory.py new file mode 100644 index 00000000000000..7bbc95e05ffbe5 --- /dev/null +++ b/src/sentry/scm/factory.py @@ -0,0 +1,16 @@ +from scm.manager import SourceCodeManager +from scm.types import Referrer, RepositoryId + +from sentry.scm.private.helpers import fetch_repository, fetch_service_provider, record_count_metric + + +def new(organization_id: int, repository_id: RepositoryId, referrer: Referrer) -> SourceCodeManager: + """Return a new SourceCodeManager instance.""" + return SourceCodeManager.make_from_repository_id( + organization_id, + repository_id, + referrer, + fetch_repository=fetch_repository, + fetch_provider=fetch_service_provider, + record_count=record_count_metric, + ) diff --git a/src/sentry/scm/private/helpers.py b/src/sentry/scm/private/helpers.py index 01280aaf005b62..96ef10a771a7ad 100644 --- a/src/sentry/scm/private/helpers.py +++ b/src/sentry/scm/private/helpers.py @@ -1,29 +1,29 @@ -from collections.abc import Callable - +import sentry_sdk from scm.providers.github.provider import GitHubProvider from scm.providers.gitlab.provider import GitLabProvider +from scm.rate_limit import RateLimitProvider +from scm.types import Provider, Repository, RepositoryId from sentry.constants import ObjectStatus -from sentry.integrations.base import IntegrationInstallation -from sentry.integrations.models.integration import Integration -from sentry.integrations.services.integration.model import RpcIntegration from sentry.integrations.services.integration.service import integration_service from sentry.models.repository import Repository as RepositoryModel -from sentry.scm.errors import SCMCodedError -from sentry.scm.private.rate_limit import RateLimitProvider, RedisRateLimitProvider -from sentry.scm.types import ExternalId, Provider, ProviderName, Repository +from sentry.scm.private.rate_limit import RedisRateLimitProvider +from sentry.utils import metrics -def map_integration_to_provider( +def fetch_service_provider( organization_id: int, - integration: Integration | RpcIntegration, repository: Repository, - get_installation: Callable[ - [Integration | RpcIntegration, int], IntegrationInstallation - ] = lambda i, oid: i.get_installation(organization_id=oid), rate_limit_provider: RateLimitProvider | None = None, -) -> Provider: - client = get_installation(integration, organization_id).get_client() +) -> Provider | None: + integration = integration_service.get_integration( + integration_id=repository["integration_id"], + organization_id=organization_id, + ) + if not integration: + return None + + client = integration.get_installation(organization_id=organization_id).get_client() if integration.provider == "github": return GitHubProvider( @@ -35,38 +35,10 @@ def map_integration_to_provider( elif integration.provider == "gitlab": return GitLabProvider(client, organization_id, repository) else: - raise SCMCodedError(integration.provider, code="unsupported_integration") - - -def map_repository_model_to_repository(repository: RepositoryModel) -> Repository: - return { - "external_id": repository.external_id, - "id": repository.id, - "integration_id": repository.integration_id, - "is_active": repository.status == ObjectStatus.ACTIVE, - "name": repository.name, - "organization_id": repository.organization_id, - "provider_name": repository.provider.removeprefix("integrations:"), - } - - -def fetch_service_provider( - organization_id: int, - repository: Repository, - map_to_provider: Callable[[Integration | RpcIntegration, int, Repository], Provider] = lambda i, - oid, - r: map_integration_to_provider(oid, i, r), -) -> Provider | None: - integration = integration_service.get_integration( - integration_id=repository["integration_id"], - organization_id=organization_id, - ) - return map_to_provider(integration, organization_id, repository) if integration else None + return None -def fetch_repository( - organization_id: int, repository_id: int | tuple[ProviderName, ExternalId] -) -> Repository | None: +def fetch_repository(organization_id: int, repository_id: RepositoryId) -> Repository | None: try: if isinstance(repository_id, int): repo = RepositoryModel.objects.get(organization_id=organization_id, id=repository_id) @@ -79,4 +51,32 @@ def fetch_repository( except RepositoryModel.DoesNotExist: return None - return map_repository_model_to_repository(repo) + return { + "external_id": repo.external_id, + "id": repo.id, + "integration_id": repo.integration_id, + "is_active": repo.status == ObjectStatus.ACTIVE, + "name": repo.name, + "organization_id": repo.organization_id, + "provider_name": repo.provider.removeprefix("integrations:"), + } + + +def report_error_to_sentry(e: Exception) -> None: + """Typing wrapper around sentry_sdk.capture_exception.""" + sentry_sdk.capture_exception(e) + + +def record_count_metric(key: str, amount: int, tags: dict[str, str]) -> None: + """Typing wrapper around metrics.incr.""" + metrics.incr(key, amount, tags=tags) + + +def record_distribution_metric(key: str, amount: int, tags: dict[str, str], unit: str) -> None: + """Typing wrapper around metrics.distribution.""" + metrics.distribution(key, amount, tags=tags, unit=unit) + + +def record_timer_metric(key: str, amount: float, tags: dict[str, str]) -> None: + """Typing wrapper around metrics.distribution.""" + metrics.distribution(key, amount, tags=tags) diff --git a/src/sentry/scm/private/ipc.py b/src/sentry/scm/private/ipc.py index 844103258eb2c0..ca3ab4f30a5842 100644 --- a/src/sentry/scm/private/ipc.py +++ b/src/sentry/scm/private/ipc.py @@ -14,10 +14,14 @@ from typing import assert_never, cast import msgspec -import sentry_sdk from sentry.scm.errors import SCMProviderEventNotSupported, SCMProviderNotSupported from sentry.scm.private.event_stream import SourceCodeManagerEventStream, scm_event_stream +from sentry.scm.private.helpers import ( + record_count_metric, + record_distribution_metric, + record_timer_metric, +) from sentry.scm.private.webhooks.github import deserialize_github_event from sentry.scm.types import ( CheckRunAction, @@ -36,7 +40,6 @@ from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import scm_tasks -from sentry.utils import metrics class SubscriptionEventParser(msgspec.Struct, gc=False, frozen=True): @@ -433,26 +436,6 @@ def run_webhook_handler_region_task( ) -def report_error_to_sentry(e: Exception) -> None: - """Typing wrapper around sentry_sdk.capture_exception.""" - sentry_sdk.capture_exception(e) - - -def record_count_metric(key: str, amount: int, tags: dict[str, str]) -> None: - """Typing wrapper around metrics.incr.""" - metrics.incr(key, amount, tags=tags) - - -def record_distribution_metric(key: str, amount: int, tags: dict[str, str], unit: str) -> None: - """Typing wrapper around metrics.distribution.""" - metrics.distribution(key, amount, tags=tags, unit=unit) - - -def record_timer_metric(key: str, amount: float, tags: dict[str, str]) -> None: - """Typing wrapper around metrics.distribution.""" - metrics.distribution(key, amount, tags=tags) - - METRIC_PREFIX = "sentry.scm.run_listener" diff --git a/src/sentry/scm/private/stream_producer.py b/src/sentry/scm/private/stream_producer.py index 7dd4fbbb8c8916..75d46ea24f2044 100644 --- a/src/sentry/scm/private/stream_producer.py +++ b/src/sentry/scm/private/stream_producer.py @@ -2,12 +2,12 @@ from collections.abc import Callable from sentry.scm.errors import SCMProviderEventNotSupported, SCMProviderNotSupported +from sentry.scm.private.helpers import report_error_to_sentry from sentry.scm.private.ipc import ( PRODUCE_TO_LISTENER, produce_to_listener, produce_to_listeners, record_count_metric, - report_error_to_sentry, ) from sentry.scm.types import HybridCloudSilo, SubscriptionEvent from sentry.scm.utils import check_rollout_option diff --git a/src/sentry/scm/types.py b/src/sentry/scm/types.py index 645694088f51ef..7920e77300807c 100644 --- a/src/sentry/scm/types.py +++ b/src/sentry/scm/types.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TypedDict +from typing import Literal, TypedDict from scm.types import ( CheckRunAction, @@ -154,3 +154,5 @@ class PullRequestEvent: type EventType = CheckRunEvent | CommentEvent | PullRequestEvent +type EventTypeHint = Literal["check_run", "comment", "pull_request"] +type HybridCloudSilo = Literal["control", "region"] diff --git a/tests/sentry/scm/endpoints/test_scm_rpc.py b/tests/sentry/scm/endpoints/test_scm_rpc.py index ffba19b20803d3..47f1fb3b8f1227 100644 --- a/tests/sentry/scm/endpoints/test_scm_rpc.py +++ b/tests/sentry/scm/endpoints/test_scm_rpc.py @@ -53,7 +53,7 @@ def _convert(self, django_response): def get(self, url, headers=None): return self._convert(self._client.get(url, headers=headers)) - def post(self, url, data=None, headers=None, stream=False): + def post(self, url, data=None, headers=None): h = dict(headers) if headers else {} content_type = h.pop("Content-Type", "application/octet-stream") return self._convert( @@ -82,9 +82,9 @@ def setUp(self) -> None: self.rpc_client = SourceCodeManager.make_from_repository_id( self.organization.id, self.repo.id, - fetch_base_url=lambda: "", - fetch_signing_secret=lambda: "a-long-value-that-is-hard-to-guess", - session_override=DjangoTestClientSessionAdapter(self.client), + base_url="", + signing_secret="a-long-value-that-is-hard-to-guess", + session=lambda: DjangoTestClientSessionAdapter(self.client), ) self.default_headers = { diff --git a/tests/sentry/scm/integration/test_helpers_integration.py b/tests/sentry/scm/integration/test_helpers_integration.py index e54d15eb587ba1..14a514ca61a626 100644 --- a/tests/sentry/scm/integration/test_helpers_integration.py +++ b/tests/sentry/scm/integration/test_helpers_integration.py @@ -1,18 +1,9 @@ -from unittest.mock import MagicMock - -import pytest -from scm.errors import SCMCodedError from scm.providers.github.provider import GitHubProvider from scm.types import Repository from sentry.constants import ObjectStatus from sentry.models.repository import Repository as RepositoryModel -from sentry.scm.private.helpers import ( - fetch_repository, - fetch_service_provider, - map_integration_to_provider, - map_repository_model_to_repository, -) +from sentry.scm.private.helpers import fetch_repository, fetch_service_provider from sentry.testutils.cases import TestCase @@ -34,9 +25,7 @@ def test_fetch_by_id_returns_repository(self) -> None: assert result["is_active"] is True def test_fetch_by_id_returns_none_for_nonexistent(self) -> None: - result = fetch_repository(self.organization.id, 99999) - - assert result is None + assert fetch_repository(self.organization.id, 99999) is None def test_fetch_by_id_returns_none_for_wrong_organization(self) -> None: other_org = self.create_organization() @@ -47,10 +36,7 @@ def test_fetch_by_id_returns_none_for_wrong_organization(self) -> None: external_id="67890", status=ObjectStatus.ACTIVE, ) - - result = fetch_repository(self.organization.id, repo.id) - - assert result is None + assert fetch_repository(self.organization.id, repo.id) is None def test_fetch_by_provider_and_external_id_returns_repository(self) -> None: RepositoryModel.objects.create( @@ -67,85 +53,7 @@ def test_fetch_by_provider_and_external_id_returns_repository(self) -> None: assert result["name"] == "test-org/test-repo" def test_fetch_by_provider_and_external_id_returns_none_for_nonexistent(self) -> None: - result = fetch_repository(self.organization.id, ("github", "nonexistent")) - - assert result is None - - -class TestMapRepositoryModelToRepository(TestCase): - def test_maps_all_fields_correctly(self) -> None: - integration = self.create_integration( - organization=self.organization, - provider="github", - name="Github Test Org", - external_id="1", - ) - repo = RepositoryModel.objects.create( - organization_id=self.organization.id, - name="test-org/test-repo", - provider="integrations:github", - external_id="12345", - status=ObjectStatus.ACTIVE, - integration_id=integration.id, - ) - - result = map_repository_model_to_repository(repo) - - assert result["integration_id"] == integration.id - assert result["name"] == "test-org/test-repo" - assert result["organization_id"] == self.organization.id - assert result["is_active"] is True - - -class TestMapIntegrationToProvider(TestCase): - def test_returns_github_provider_for_github_integration(self) -> None: - integration = self.create_integration( - organization=self.organization, - provider="github", - name="Github Test Org", - external_id="1", - ) - repository: Repository = { - "integration_id": integration.id, - "name": "test-org/test-repo", - "organization_id": self.organization.id, - "is_active": False, - "external_id": None, - } - - provider = map_integration_to_provider( - self.organization.id, - integration, - repository, - get_installation=lambda _, oid: MagicMock(), - ) - - assert isinstance(provider, GitHubProvider) - - def test_raises_error_for_unsupported_provider(self) -> None: - integration = self.create_integration( - organization=self.organization, - provider="integrations:github", - name="Unsupported Provider Test", - external_id="1", - ) - repository: Repository = { - "integration_id": integration.id, - "name": "test-org/test-repo", - "organization_id": self.organization.id, - "is_active": False, - "external_id": None, - } - - with pytest.raises(SCMCodedError) as exc_info: - map_integration_to_provider( - self.organization.id, - integration, - repository, - get_installation=lambda _, oid: MagicMock(), - ) - - assert exc_info.value.code == "unsupported_integration" + assert fetch_repository(self.organization.id, ("github", "nonexistent")) is None class TestFetchServiceProvider(TestCase): @@ -167,9 +75,6 @@ def test_returns_provider_from_map_to_provider(self) -> None: provider = fetch_service_provider( self.organization.id, repository, - map_to_provider=lambda i, oid, r: map_integration_to_provider( - oid, i, r, get_installation=lambda _, __: MagicMock() - ), ) assert isinstance(provider, GitHubProvider) @@ -184,19 +89,3 @@ def test_returns_none_for_nonexistent_integration(self) -> None: } result = fetch_service_provider(self.organization.id, repository) assert result is None - - -def _make_active_repository(organization_id: int) -> Repository: - return { - "integration_id": 1, - "name": "test-org/test-repo", - "organization_id": organization_id, - "is_active": True, - "external_id": None, - } - - -def _make_provider(is_rate_limited: bool = False): - provider = MagicMock() - provider.is_rate_limited.return_value = is_rate_limited - return provider diff --git a/uv.lock b/uv.lock index 34ab61bcf61bdb..6cf9b1f22686ab 100644 --- a/uv.lock +++ b/uv.lock @@ -2207,6 +2207,7 @@ dependencies = [ { name = "sentry-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentry-redis-tools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentry-relay", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sentry-scm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentry-sdk", extra = ["http2"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentry-usage-accountant", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2375,6 +2376,7 @@ requires-dist = [ { name = "sentry-protos", specifier = ">=0.8.10" }, { name = "sentry-redis-tools", specifier = ">=0.5.0" }, { name = "sentry-relay", specifier = ">=0.9.25" }, + { name = "sentry-scm", specifier = ">=0.1.7" }, { name = "sentry-sdk", extras = ["http2"], specifier = ">=2.47.0" }, { name = "sentry-usage-accountant", specifier = ">=0.0.10" }, { name = "setuptools", specifier = ">=70.0.0" }, @@ -2581,6 +2583,18 @@ wheels = [ { url = "https://pypi.devinfra.sentry.io/wheels/sentry_relay-0.9.25-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b2163d80ae6fea052e8e7444e16dfdd25a5e7ce02f8625a433cfe739da96a262" }, ] +[[package]] +name = "sentry-scm" +version = "0.1.7" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +dependencies = [ + { name = "msgspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/sentry_scm-0.1.7-py3-none-any.whl", hash = "sha256:afed9d4087b98ce29b1c6ea08266149bf68e0fe72806765889690ecae270c847" }, +] + [[package]] name = "sentry-sdk" version = "2.47.0" From 365b77de0e314a7dbcc5d6a3dd48fbabc3eedb99 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:18:05 +0000 Subject: [PATCH 4/6] :hammer_and_wrench: Sync API Urls to TypeScript --- static/app/utils/api/knownSentryApiUrls.generated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/api/knownSentryApiUrls.generated.ts b/static/app/utils/api/knownSentryApiUrls.generated.ts index b49d883dcd04ec..10a377f6431b3e 100644 --- a/static/app/utils/api/knownSentryApiUrls.generated.ts +++ b/static/app/utils/api/knownSentryApiUrls.generated.ts @@ -118,7 +118,7 @@ export type KnownSentryApiUrls = | '/internal/project-config/' | '/internal/projectkey-cell-mappings/' | '/internal/rpc/$serviceName/$methodName/' - | '/internal/scm-rpc/$methodName/' + | '/internal/scm-rpc/' | '/internal/seer-rpc/$methodName/' | '/internal/seer/night-shift/trigger/' | '/internal/warnings/' From 95f23b7ceda0e74fd83c05ba0c67501e24a9085f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 14 Apr 2026 15:22:22 -0500 Subject: [PATCH 5/6] Fix import --- src/sentry/scm/private/stream_producer.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/sentry/scm/private/stream_producer.py b/src/sentry/scm/private/stream_producer.py index 75d46ea24f2044..c03c5fa517c936 100644 --- a/src/sentry/scm/private/stream_producer.py +++ b/src/sentry/scm/private/stream_producer.py @@ -2,13 +2,8 @@ from collections.abc import Callable from sentry.scm.errors import SCMProviderEventNotSupported, SCMProviderNotSupported -from sentry.scm.private.helpers import report_error_to_sentry -from sentry.scm.private.ipc import ( - PRODUCE_TO_LISTENER, - produce_to_listener, - produce_to_listeners, - record_count_metric, -) +from sentry.scm.private.helpers import record_count_metric, report_error_to_sentry +from sentry.scm.private.ipc import PRODUCE_TO_LISTENER, produce_to_listener, produce_to_listeners from sentry.scm.types import HybridCloudSilo, SubscriptionEvent from sentry.scm.utils import check_rollout_option From 958e8f56d426cb65ed823c434d7187969af8e8f3 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 14 Apr 2026 15:39:09 -0500 Subject: [PATCH 6/6] Fix types --- src/sentry/scm/endpoints/scm_rpc.py | 6 +++--- src/sentry/scm/factory.py | 2 +- src/sentry/scm/private/helpers.py | 26 +++++++++++++++++--------- src/sentry/scm/private/ipc.py | 6 +----- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/sentry/scm/endpoints/scm_rpc.py b/src/sentry/scm/endpoints/scm_rpc.py index a8892c4ca26446..60941f25bc001f 100644 --- a/src/sentry/scm/endpoints/scm_rpc.py +++ b/src/sentry/scm/endpoints/scm_rpc.py @@ -15,7 +15,7 @@ ) server = RpcServer( - secrets=settings.SCM_RPC_SHARED_SECRET, + secrets=settings.SCM_RPC_SHARED_SECRET or [], fetch_repository=fetch_repository, fetch_provider=fetch_service_provider, record_count=record_count_metric, @@ -38,10 +38,10 @@ class ScmRpcServiceEndpoint(Endpoint): @sentry_sdk.trace def get(self, request: Request) -> HttpResponse: - resp = server.get(headers=request.headers) + resp = server.get(headers={k: v for k, v in request.headers.items()}) return HttpResponse(content=resp.content, status=resp.status_code, headers=resp.headers) @sentry_sdk.trace def post(self, request: Request) -> StreamingHttpResponse: - resp = server.post(data=request.body, headers=request.headers) + resp = server.post(request.body, headers={k: v for k, v in request.headers.items()}) return StreamingHttpResponse(resp.content, status=resp.status_code, headers=resp.headers) diff --git a/src/sentry/scm/factory.py b/src/sentry/scm/factory.py index 7bbc95e05ffbe5..93c6cd2db5e6f2 100644 --- a/src/sentry/scm/factory.py +++ b/src/sentry/scm/factory.py @@ -9,7 +9,7 @@ def new(organization_id: int, repository_id: RepositoryId, referrer: Referrer) - return SourceCodeManager.make_from_repository_id( organization_id, repository_id, - referrer, + referrer=referrer, fetch_repository=fetch_repository, fetch_provider=fetch_service_provider, record_count=record_count_metric, diff --git a/src/sentry/scm/private/helpers.py b/src/sentry/scm/private/helpers.py index 96ef10a771a7ad..e25e480a146aae 100644 --- a/src/sentry/scm/private/helpers.py +++ b/src/sentry/scm/private/helpers.py @@ -1,3 +1,5 @@ +from typing import cast + import sentry_sdk from scm.providers.github.provider import GitHubProvider from scm.providers.gitlab.provider import GitLabProvider @@ -51,15 +53,21 @@ def fetch_repository(organization_id: int, repository_id: RepositoryId) -> Repos except RepositoryModel.DoesNotExist: return None - return { - "external_id": repo.external_id, - "id": repo.id, - "integration_id": repo.integration_id, - "is_active": repo.status == ObjectStatus.ACTIVE, - "name": repo.name, - "organization_id": repo.organization_id, - "provider_name": repo.provider.removeprefix("integrations:"), - } + provider = repo.provider + assert isinstance(provider, str) + + return cast( + Repository, + { + "external_id": repo.external_id, + "id": repo.id, + "integration_id": repo.integration_id, + "is_active": repo.status == ObjectStatus.ACTIVE, + "name": repo.name, + "organization_id": repo.organization_id, + "provider_name": provider.removeprefix("integrations:"), + }, + ) def report_error_to_sentry(e: Exception) -> None: diff --git a/src/sentry/scm/private/ipc.py b/src/sentry/scm/private/ipc.py index ca3ab4f30a5842..b52aa4791995a4 100644 --- a/src/sentry/scm/private/ipc.py +++ b/src/sentry/scm/private/ipc.py @@ -14,6 +14,7 @@ from typing import assert_never, cast import msgspec +from scm.types import CheckRunAction, CommentAction, CommentType, ProviderName, PullRequestAction from sentry.scm.errors import SCMProviderEventNotSupported, SCMProviderNotSupported from sentry.scm.private.event_stream import SourceCodeManagerEventStream, scm_event_stream @@ -24,16 +25,11 @@ ) from sentry.scm.private.webhooks.github import deserialize_github_event from sentry.scm.types import ( - CheckRunAction, CheckRunEvent, - CommentAction, CommentEvent, - CommentType, EventType, EventTypeHint, HybridCloudSilo, - ProviderName, - PullRequestAction, PullRequestEvent, SubscriptionEvent, )