Skip to content

Commit 024450e

Browse files
authored
feat(tempest): Endpoint for creating tempest credentials (#82224)
Endpoint for creating tempest credentials. There will be another PR which handles deletion of credentials. Part of getsentry/team-gdx#52 --------- Signed-off-by: Vjeran Grozdanic <[email protected]>
1 parent 7b60f8b commit 024450e

File tree

3 files changed

+129
-2
lines changed

3 files changed

+129
-2
lines changed

src/sentry/tempest/endpoints/tempest_credentials.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.db import IntegrityError
12
from rest_framework.exceptions import NotFound
23
from rest_framework.request import Request
34
from rest_framework.response import Response
@@ -12,13 +13,14 @@
1213
from sentry.models.project import Project
1314
from sentry.tempest.models import TempestCredentials
1415
from sentry.tempest.permissions import TempestCredentialsPermission
15-
from sentry.tempest.serializers import TempestCredentialsSerializer
16+
from sentry.tempest.serializers import DRFTempestCredentialsSerializer, TempestCredentialsSerializer
1617

1718

1819
@region_silo_endpoint
1920
class TempestCredentialsEndpoint(ProjectEndpoint):
2021
publish_status = {
2122
"GET": ApiPublishStatus.PRIVATE,
23+
"POST": ApiPublishStatus.PRIVATE,
2224
}
2325
owner = ApiOwner.GDX
2426

@@ -40,3 +42,17 @@ def get(self, request: Request, project: Project) -> Response:
4042
on_results=lambda x: serialize(x, request.user, TempestCredentialsSerializer()),
4143
paginator_cls=OffsetPaginator,
4244
)
45+
46+
def post(self, request: Request, project: Project) -> Response:
47+
if not self.has_feature(request, project):
48+
raise NotFound
49+
50+
serializer = DRFTempestCredentialsSerializer(data=request.data)
51+
serializer.is_valid(raise_exception=True)
52+
try:
53+
serializer.save(created_by_id=request.user.id, project=project)
54+
except IntegrityError:
55+
return Response(
56+
{"detail": "A credential with this client ID already exists."}, status=400
57+
)
58+
return Response(serializer.data, status=201)

src/sentry/tempest/serializers.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from rest_framework import serializers
2+
13
from sentry.api.serializers.base import Serializer, register
24
from sentry.tempest.models import TempestCredentials
35

@@ -17,3 +19,24 @@ def serialize(self, obj, attrs, user, **kwargs):
1719
"latestFetchedItemId": obj.latest_fetched_item_id,
1820
"createdById": obj.created_by_id,
1921
}
22+
23+
24+
class DRFTempestCredentialsSerializer(serializers.ModelSerializer):
25+
clientId = serializers.CharField(source="client_id")
26+
clientSecret = serializers.CharField(source="client_secret")
27+
message = serializers.CharField(read_only=True)
28+
messageType = serializers.CharField(source="message_type", read_only=True)
29+
latestFetchedItemId = serializers.CharField(source="latest_fetched_item_id", read_only=True)
30+
createdById = serializers.CharField(source="created_by_id", read_only=True)
31+
32+
class Meta:
33+
model = TempestCredentials
34+
fields = [
35+
"id",
36+
"clientId",
37+
"clientSecret",
38+
"message",
39+
"messageType",
40+
"latestFetchedItemId",
41+
"createdById",
42+
]

tests/sentry/tempest/endpoints/test_tempest_credentials.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
from sentry.tempest.models import TempestCredentials
12
from sentry.testutils.cases import APITestCase
23
from sentry.testutils.helpers.features import Feature
34

45

56
class TestTempestCredentials(APITestCase):
67
endpoint = "sentry-api-0-project-tempest-credentials"
78

8-
def test_create_tempest_credentials(self):
9+
valid_credentials_data = {"clientId": "test", "clientSecret": "test"}
10+
11+
def test_get_tempest_credentials(self):
912
with Feature({"organizations:tempest-access": True}):
1013
credentials = [self.create_tempest_credentials(self.project) for _ in range(5)]
1114

@@ -34,3 +37,88 @@ def test_client_secret_is_obfuscated(self):
3437

3538
def test_unauthenticated_user_cant_access_endpoint(self):
3639
self.get_error_response(self.project.organization.slug, self.project.slug)
40+
41+
def test_create_tempest_credentials(self):
42+
with Feature({"organizations:tempest-access": True}):
43+
self.login_as(self.user)
44+
response = self.get_success_response(
45+
self.project.organization.slug,
46+
self.project.slug,
47+
method="POST",
48+
**self.valid_credentials_data,
49+
)
50+
assert response.status_code == 201
51+
creds_obj = TempestCredentials.objects.get(project=self.project)
52+
assert creds_obj.client_id == self.valid_credentials_data["clientId"]
53+
assert creds_obj.client_secret == self.valid_credentials_data["clientSecret"]
54+
assert creds_obj.project == self.project
55+
assert creds_obj.created_by_id == self.user.id
56+
57+
def test_create_tempest_credentials_without_feature_flag(self):
58+
self.login_as(self.user)
59+
response = self.get_error_response(
60+
self.project.organization.slug,
61+
self.project.slug,
62+
method="POST",
63+
**self.valid_credentials_data,
64+
)
65+
assert response.status_code == 404
66+
67+
def test_create_tempest_credentials_as_unauthenticated_user(self):
68+
response = self.get_error_response(
69+
self.project.organization.slug,
70+
self.project.slug,
71+
method="POST",
72+
**self.valid_credentials_data,
73+
)
74+
assert response.status_code == 401
75+
76+
def test_non_admin_cant_create_tempest_credentials(self):
77+
non_admin_user = self.create_user()
78+
self.create_member(
79+
user=non_admin_user, organization=self.project.organization, role="member"
80+
)
81+
with Feature({"organizations:tempest-access": True}):
82+
self.login_as(non_admin_user)
83+
response = self.get_error_response(
84+
self.project.organization.slug,
85+
self.project.slug,
86+
method="POST",
87+
**self.valid_credentials_data,
88+
)
89+
assert response.status_code == 403
90+
91+
def test_create_tempest_credentials_with_invalid_data(self):
92+
with Feature({"organizations:tempest-access": True}):
93+
self.login_as(self.user)
94+
response = self.get_error_response(
95+
self.project.organization.slug,
96+
self.project.slug,
97+
method="POST",
98+
**{"clientId": "test"},
99+
)
100+
assert response.status_code == 400
101+
102+
response2 = self.get_error_response(
103+
self.project.organization.slug,
104+
self.project.slug,
105+
method="POST",
106+
**{"clientSecret": "test"},
107+
)
108+
assert response2.status_code == 400
109+
110+
def test_cant_create_tempest_credentials_with_duplicate_client_id(self):
111+
with Feature({"organizations:tempest-access": True}):
112+
self.login_as(self.user)
113+
self.create_tempest_credentials(
114+
self.project, client_id=self.valid_credentials_data["clientId"]
115+
)
116+
response = self.get_error_response(
117+
self.project.organization.slug,
118+
self.project.slug,
119+
method="POST",
120+
**self.valid_credentials_data,
121+
)
122+
# database constraint violation
123+
assert response.status_code == 400
124+
assert response.data["detail"] == "A credential with this client ID already exists."

0 commit comments

Comments
 (0)