Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d4cac53

Browse files
✨ add webhook handle view
1 parent 4852388 commit d4cac53

File tree

2 files changed

+112
-21
lines changed

2 files changed

+112
-21
lines changed

‎gitlab_webhooks/views.py‎

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,66 @@
1-
from django import views
1+
import json
2+
3+
from django.conf import settings
4+
from django.core.exceptions import ImproperlyConfigured
5+
from django.dispatch import Signal
6+
from django.utils.decorators import method_decorator
7+
from django.views import View
28
from django.http import HttpRequest, JsonResponse
3-
from gitlab_webhooks import __version__
9+
from django.views.decorators.csrf import csrf_exempt
410

11+
from . import constants, signals
512

6-
class HealthCheckView(views.View):
7-
def get(self, request: HttpRequest):
8-
return JsonResponse({"detail": "ok"})
913

14+
@method_decorator(csrf_exempt, "dispatch")
15+
class WebhookView(View):
16+
def get_secret(self) -> str:
17+
"""
18+
Returns webhook's secret key.
19+
"""
20+
secret = settings.DJANGO_GITLAB_WEBHOOKS.get("SECRET")
21+
if not secret:
22+
raise ImproperlyConfigured("SECRET key is not specified!")
23+
else:
24+
return secret
25+
26+
@classmethod
27+
def event_is_allowed(cls, event: str) -> bool:
28+
if event in settings.DJANGO_GITLAB_WEBHOOKS["ALLOWED_EVENTS"]:
29+
return True
30+
else:
31+
return False
1032

11-
health_check_view = HealthCheckView.as_view()
33+
@classmethod
34+
def get_signal(cls, event: str) -> Signal:
35+
formatted_event_name = event.lower().replace(" hook", "").replace(" ", "_")
36+
return getattr(signals, formatted_event_name)
1237

38+
def post(self, request: HttpRequest, **kwargs) -> JsonResponse:
39+
# Validate webhook secret
40+
if request.META.get(constants.TOKEN_HEADER) != self.get_secret():
41+
return JsonResponse(
42+
{"detail": constants.INVALID_HTTP_X_GITLAB_TOKEN},
43+
status=400,
44+
)
1345

14-
class AppVersionView(views.View):
15-
def get(self, request: HttpRequest):
16-
return JsonResponse({"detail": __version__})
46+
# Check event header
47+
event = request.META.get(constants.EVENT_HEADER)
48+
if event is None:
49+
return JsonResponse(
50+
{"detail": constants.EVENT_HEADER_IS_MISSING},
51+
status=400,
52+
)
1753

54+
# Validate that event is allowed
55+
event_is_allowed = self.event_is_allowed(event)
56+
if event_is_allowed is False:
57+
return JsonResponse(
58+
{"detail": constants.EVENT_IS_NOT_ALLOWED.format(event=event)},
59+
status=400,
60+
)
1861

19-
app_version_view = AppVersionView.as_view()
62+
# Send signal on success event
63+
signal = self.get_signal(event)
64+
signal.send(__class__, payload=json.loads(request.body))
65+
66+
return JsonResponse({"detail": "ok"})

‎tests/test_views.py‎

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,65 @@
11
import json
22

33
import pytest
4+
from django.core.exceptions import ImproperlyConfigured
45
from django.test import RequestFactory
5-
from gitlab_webhooks import __version__, views
6+
from gitlab_webhooks.views import WebhookView
7+
from gitlab_webhooks import constants
68

79

8-
def test_health_check_view(rf: RequestFactory):
9-
request = rf.get("/fake-url/")
10-
response = views.HealthCheckView().get(request)
10+
class TestWebhookView:
11+
@pytest.mark.parametrize("event", constants.Events.values())
12+
def test_success(self, rf: RequestFactory, settings, event: str):
13+
settings.DJANGO_GITLAB_WEBHOOKS = {
14+
"SECRET": "fake_token",
15+
"ALLOWED_EVENTS": [event],
16+
}
17+
headers = {
18+
constants.TOKEN_HEADER: "fake_token",
19+
constants.EVENT_HEADER: event,
20+
}
21+
request = rf.post("/fake-url/", **headers)
22+
request._body = '{"test": "ok"}'.encode()
23+
response = WebhookView().post(request)
1124

12-
assert response.status_code == 200
13-
assert json.loads(response.content) == {"detail": "ok"}
25+
assert response.status_code == 200
26+
assert json.loads(response.content) == {"detail": "ok"}
1427

28+
def test_get_secret(self, settings):
29+
settings.DJANGO_GITLAB_WEBHOOKS = {}
1530

16-
def test_app_version_view(rf: RequestFactory):
17-
request = rf.get("/fake-url/")
18-
response = views.AppVersionView().get(request)
31+
with pytest.raises(ImproperlyConfigured):
32+
WebhookView().get_secret()
1933

20-
assert response.status_code == 200
21-
assert json.loads(response.content) == {"detail": __version__}
34+
def test_invalid_token(self, rf: RequestFactory, settings):
35+
settings.DJANGO_GITLAB_WEBHOOKS = {"SECRET": "fake_token"}
36+
headers = {constants.TOKEN_HEADER: "invalid"}
37+
request = rf.post("/fake-url/", **headers)
38+
response = WebhookView().post(request)
39+
40+
assert response.status_code == 400
41+
assert json.loads(response.content.decode()) == {
42+
"detail": constants.INVALID_HTTP_X_GITLAB_TOKEN
43+
}
44+
45+
def test_event_header_is_missing(self, rf: RequestFactory, settings):
46+
settings.DJANGO_GITLAB_WEBHOOKS = {"SECRET": "fake_token"}
47+
headers = {constants.TOKEN_HEADER: "fake_token"}
48+
request = rf.post("/fake-url/", **headers)
49+
response = WebhookView().post(request)
50+
51+
assert response.status_code == 400
52+
assert json.loads(response.content.decode()) == {
53+
"detail": constants.EVENT_HEADER_IS_MISSING
54+
}
55+
56+
def test_event_is_not_allowed(self, rf: RequestFactory, settings):
57+
settings.DJANGO_GITLAB_WEBHOOKS = {"SECRET": "fake_token", "ALLOWED_EVENTS": []}
58+
headers = {constants.TOKEN_HEADER: "fake_token", constants.EVENT_HEADER: "test"}
59+
request = rf.post("/fake-url/", **headers)
60+
response = WebhookView().post(request)
61+
62+
assert response.status_code == 400
63+
assert json.loads(response.content.decode()) == {
64+
"detail": constants.EVENT_IS_NOT_ALLOWED.format(event="test")
65+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /