Files
2024-02-15 17:01:50 +00:00

437 lines
17 KiB
Python

from time import time
from unittest import mock
from unittest.mock import call
from urllib.parse import parse_qs, quote_plus
import pytest
import responses
from django.test import override_settings
from responses import matchers
from fixtures.vsts import VstsIntegrationTestCase
from sentry.integrations.vsts.client import VstsApiClient
from sentry.integrations.vsts.integration import VstsIntegrationProvider
from sentry.models.identity import Identity, IdentityProvider
from sentry.models.integrations.integration import Integration
from sentry.models.repository import Repository
from sentry.shared_integrations.exceptions import ApiError
from sentry.silo.base import SiloMode
from sentry.silo.util import PROXY_BASE_PATH, PROXY_OI_HEADER, PROXY_PATH, PROXY_SIGNATURE_HEADER
from sentry.testutils.silo import assume_test_silo_mode, control_silo_test, region_silo_test
from sentry.utils import json
@control_silo_test
class VstsApiClientTest(VstsIntegrationTestCase):
@pytest.fixture(autouse=True)
def _setup_metric_patch(self):
with mock.patch("sentry.shared_integrations.track_response.metrics") as self.metrics:
yield
def test_refreshes_expired_token(self):
self.assert_installation()
integration = Integration.objects.get(provider="vsts")
# Make the Identity have an expired token
idp = IdentityProvider.objects.get(external_id=self.vsts_account_id)
identity = Identity.objects.get(idp_id=idp.id)
identity.data["expires"] = int(time()) - int(123456789)
identity.save()
# New values VSTS will return on refresh
self.access_token = "new-access-token"
self.refresh_token = "new-refresh-token"
self._stub_vsts()
# Make a request with expired token
integration.get_installation(
integration.organizationintegration_set.first().organization_id
).get_client(base_url=self.vsts_base_url).get_projects()
# Second to last request, before the Projects request, was to refresh
# the Access Token.
assert responses.calls[-2].request.url == "https://app.vssps.visualstudio.com/oauth2/token"
# Then we request the Projects with the new token
assert (
responses.calls[-1].request.url.split("?")[0]
== f"{self.vsts_base_url.lower()}_apis/projects"
)
identity = Identity.objects.get(id=identity.id)
assert identity.scopes == [
"vso.code",
"vso.graph",
"vso.serviceendpoint_manage",
"vso.work_write",
]
assert identity.data["access_token"] == "new-access-token"
assert identity.data["refresh_token"] == "new-refresh-token"
assert identity.data["expires"] > int(time())
@responses.activate
def test_does_not_refresh_valid_tokens(self):
self.assert_installation()
responses.reset()
integration = Integration.objects.get(provider="vsts")
# Make the Identity have a non-expired token
idp = IdentityProvider.objects.get(external_id=self.vsts_account_id)
identity = Identity.objects.get(idp_id=idp.id)
expires = int(time()) + int(123456789)
identity.data["expires"] = expires
access_token = identity.data["access_token"]
refresh_token = identity.data["refresh_token"]
identity.save()
# New values VSTS will return on refresh
self.access_token = "new-access-token"
self.refresh_token = "new-refresh-token"
self._stub_vsts()
# Make a request
integration.get_installation(
integration.organizationintegration_set.first().organization_id
).get_client(base_url=self.vsts_base_url).get_projects()
assert len(responses.calls) == 1
assert (
responses.calls[0].request.url
== "https://myvstsaccount.visualstudio.com/_apis/projects?stateFilter=WellFormed&%24skip=0&%24top=100"
)
assert identity.data["access_token"] == access_token != self.access_token
assert identity.data["refresh_token"] == refresh_token != self.refresh_token
assert identity.data["expires"] == expires
def test_project_pagination(self):
def request_callback(request):
query = parse_qs(request.url.split("?")[1])
# allow for 220 responses
if int(query["$skip"][0]) >= 200:
projects = [self.project_a, self.project_b] * 10
else:
projects = [self.project_a, self.project_b] * 50
resp_body = {"value": projects, "count": len(projects)}
return (200, {}, json.dumps(resp_body))
self.assert_installation()
responses.reset()
integration = Integration.objects.get(provider="vsts")
responses.add_callback(
responses.GET,
f"https://{self.vsts_account_name.lower()}.visualstudio.com/_apis/projects",
callback=request_callback,
)
projects = (
integration.get_installation(
integration.organizationintegration_set.first().organization_id
)
.get_client(base_url=self.vsts_base_url)
.get_projects()
)
assert len(projects) == 220
@responses.activate
def test_simple(self):
responses.add(
responses.GET,
"https://myvstsaccount.visualstudio.com/_apis/git/repositories/albertos-apples/commits",
body=b"{}",
match=[matchers.query_param_matcher({"commit": "b", "$top": "10"})],
)
self.assert_installation()
integration = Integration.objects.get(provider="vsts")
with assume_test_silo_mode(SiloMode.REGION):
repo = Repository.objects.create(
provider="visualstudio",
name="example",
organization_id=self.organization.id,
config={
"instance": self.vsts_base_url,
"project": "project-name",
"name": "example",
},
integration_id=integration.id,
external_id="albertos-apples",
)
client = integration.get_installation(
integration.organizationintegration_set.first().organization_id
).get_client(base_url=self.vsts_base_url)
responses.calls.reset()
client.get_commits(repo_id=repo.external_id, commit="b", limit=10)
assert len(responses.calls) == 1
# Check if metrics is generated properly
calls = [
call(
"integrations.http_response",
sample_rate=1.0,
tags={"integration": "vsts", "status": 200},
),
call(
"integrations.http_response",
sample_rate=1.0,
tags={"integration": "vsts", "status": 200},
),
call(
"integrations.http_response",
sample_rate=1.0,
tags={"integration": "vsts", "status": 200},
),
call(
"integrations.http_response",
sample_rate=1.0,
tags={"integration": "vsts", "status": 200},
),
call(
"integrations.http_response",
sample_rate=1.0,
tags={"integration": "vsts", "status": 200},
),
]
assert self.metrics.incr.mock_calls == calls
@responses.activate
def test_check_file(self):
self.assert_installation()
integration = Integration.objects.get(provider="vsts")
with assume_test_silo_mode(SiloMode.REGION):
repo = Repository.objects.create(
provider="visualstudio",
name="example",
organization_id=self.organization.id,
config={
"instance": self.vsts_base_url,
"project": "project-name",
"name": "example",
},
integration_id=integration.id,
external_id="albertos-apples",
)
client = integration.get_installation(
integration.organizationintegration_set.first().organization_id
).get_client(base_url=self.vsts_base_url)
path = "src/sentry/integrations/vsts/client.py"
version = "master"
url = f"https://myvstsaccount.visualstudio.com/project-name/_apis/git/repositories/{repo.name}/items?path={path}&api-version=7.0&versionDescriptor.version={version}"
responses.add(
method=responses.GET,
url=url,
json={"text": 200},
)
resp = client.check_file(repo, path, version)
assert resp.status_code == 200
@responses.activate
def test_check_no_file(self):
self.assert_installation()
integration = Integration.objects.get(provider="vsts")
with assume_test_silo_mode(SiloMode.REGION):
repo = Repository.objects.create(
provider="visualstudio",
name="example",
organization_id=self.organization.id,
config={
"instance": self.vsts_base_url,
"project": "project-name",
"name": "example",
},
integration_id=integration.id,
external_id="albertos-apples",
)
client = integration.get_installation(
integration.organizationintegration_set.first().organization_id
).get_client(base_url=self.vsts_base_url)
path = "src/sentry/integrations/vsts/client.py"
version = "master"
url = f"https://myvstsaccount.visualstudio.com/project-name/_apis/git/repositories/{repo.name}/items?path={path}&api-version=7.0&versionDescriptor.version={version}"
responses.add(method=responses.HEAD, url=url, status=404)
with pytest.raises(ApiError):
client.check_file(repo, path, version)
@responses.activate
def test_get_stacktrace_link(self):
self.assert_installation()
integration = Integration.objects.get(provider="vsts")
installation = integration.get_installation(
integration.organizationintegration_set.first().organization_id
)
with assume_test_silo_mode(SiloMode.REGION):
repo = Repository.objects.create(
provider="visualstudio",
name="example",
organization_id=self.organization.id,
config={
"instance": self.vsts_base_url,
"project": "project-name",
"name": "example",
},
integration_id=integration.id,
external_id="albertos-apples",
)
path = "/src/sentry/integrations/vsts/client.py"
version = "master"
url = f"https://myvstsaccount.visualstudio.com/project-name/_apis/git/repositories/{repo.name}/items?path={path.lstrip('/')}&api-version=7.0&versionDescriptor.version={version}"
responses.add(
method=responses.GET,
url=url,
json={"text": 200},
)
source_url = installation.get_stacktrace_link(repo, path, "master", version)
assert (
source_url
== f"https://MyVSTSAccount.visualstudio.com/project-name/_git/{repo.name}?path={quote_plus(path)}&version=GB{version}"
)
def assert_proxy_request(request, is_proxy=True):
assert (PROXY_BASE_PATH in request.url) == is_proxy
assert (PROXY_OI_HEADER in request.headers) == is_proxy
assert (PROXY_SIGNATURE_HEADER in request.headers) == is_proxy
assert ("Authorization" in request.headers) != is_proxy
if is_proxy:
assert request.headers[PROXY_OI_HEADER] is not None
@region_silo_test
class VstsProxyApiClientTest(VstsIntegrationTestCase):
def setUp(self):
super().setUp()
self.integration, _, _, _ = self.create_identity_integration(
user=self.user,
organization=self.organization,
integration_params={
"provider": "vsts",
"external_id": "vsts:1",
"name": "fabrikam-fiber-inc",
"metadata": {
"domain_name": "https://fabrikam-fiber-inc.visualstudio.com/",
"default_project": "0987654321",
},
},
identity_params={
"external_id": "vsts",
"data": {"access_token": self.access_token, "expires": time() + 1234567},
},
)
@responses.activate
def test_integration_proxy_is_active(self):
responses.add(
responses.GET,
"https://myvstsaccount.visualstudio.com/_apis/git/repositories/albertos-apples/commits",
body=b"{}",
match=[
matchers.query_param_matcher(
{"commit": "b", "$top": "10"},
),
matchers.header_matcher(
{
"Accept": "application/json; api-version=4.1",
"Content-Type": "application/json",
"X-HTTP-Method-Override": "GET",
"X-TFS-FedAuthRedirect": "Suppress",
"Authorization": f"Bearer {self.access_token}",
}
),
],
)
responses.add(
responses.GET,
"http://controlserver/api/0/internal/integration-proxy/",
body=b"{}",
match=[
matchers.header_matcher(
{
"Accept": "application/json; api-version=4.1",
"Content-Type": "application/json",
"X-HTTP-Method-Override": "GET",
"X-TFS-FedAuthRedirect": "Suppress",
PROXY_PATH: "_apis/git/repositories/albertos-apples/commits?commit=b&%24top=10",
}
),
],
)
self.assert_installation()
installation = self.integration.get_installation(self.organization.id)
repo = Repository.objects.create(
provider="visualstudio",
name="example",
organization_id=self.organization.id,
config={"instance": self.vsts_base_url, "project": "project-name", "name": "example"},
integration_id=self.integration.id,
external_id="albertos-apples",
)
class VstsProxyApiTestClient(VstsApiClient):
_use_proxy_url_for_tests = True
client_kwargs = {
"base_url": self.vsts_base_url,
"oauth_redirect_url": VstsIntegrationProvider.oauth_redirect_url,
"org_integration_id": installation.org_integration.id,
"identity_id": installation.org_integration.default_auth_id,
}
responses.calls.reset()
with override_settings(SILO_MODE=SiloMode.MONOLITH):
client = VstsProxyApiTestClient(**client_kwargs)
client.get_commits(repo_id=repo.external_id, commit="b", limit=10)
assert len(responses.calls) == 1
request = responses.calls[0].request
assert (
"https://myvstsaccount.visualstudio.com/_apis/git/repositories/albertos-apples/commits?commit=b&%24top=10"
== request.url
)
assert client.base_url and (client.base_url.lower() in request.url)
assert_proxy_request(request, is_proxy=False)
responses.calls.reset()
with override_settings(SILO_MODE=SiloMode.CONTROL):
client = VstsProxyApiTestClient(**client_kwargs)
client.get_commits(repo_id=repo.external_id, commit="b", limit=10)
assert len(responses.calls) == 1
request = responses.calls[0].request
assert (
"https://myvstsaccount.visualstudio.com/_apis/git/repositories/albertos-apples/commits?commit=b&%24top=10"
== request.url
)
assert client.base_url and (client.base_url.lower() in request.url)
assert_proxy_request(request, is_proxy=False)
responses.calls.reset()
with override_settings(SILO_MODE=SiloMode.REGION):
client = VstsProxyApiTestClient(**client_kwargs)
client.get_commits(repo_id=repo.external_id, commit="b", limit=10)
assert len(responses.calls) == 1
request = responses.calls[0].request
assert request.url == "http://controlserver/api/0/internal/integration-proxy/"
assert (
request.headers[PROXY_PATH]
== "_apis/git/repositories/albertos-apples/commits?commit=b&%24top=10"
)
assert client.base_url and (client.base_url.lower() not in request.url)
assert_proxy_request(request, is_proxy=True)