Skip to content

Commit ee4c1ec

Browse files
committed
feat: endpoints with authorization, missing tests
1 parent 46debc8 commit ee4c1ec

File tree

8 files changed

+161
-125
lines changed

8 files changed

+161
-125
lines changed

Diff for: authentication/decorators.py

+28-5
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,37 @@
77

88
def cognito_authenticated(func):
99
@wraps(func)
10-
@api_view(["GET", "POST", "PUT", "DELETE"])
11-
@authentication_classes([CognitoAuthentication])
12-
def wrapper(request, *args, **kwargs):
13-
if not request.user or not hasattr(request.user, "get"):
10+
def wrapper(self, request, *args, **kwargs):
11+
auth = CognitoAuthentication()
12+
user, auth_error = auth.authenticate(request)
13+
if auth_error or not user:
1414
return Response(
1515
{"error": "Authentication credentials were not provided or are invalid."},
1616
status=status.HTTP_401_UNAUTHORIZED,
1717
)
18-
return func(request, *args, **kwargs)
18+
request.user = user
19+
return func(self, request, *args, **kwargs)
1920

2021
return wrapper
22+
23+
24+
# from functools import wraps
25+
# from rest_framework.response import Response
26+
# from rest_framework import status
27+
# from rest_framework.decorators import api_view, authentication_classes
28+
# from .services.cognito_authentication import CognitoAuthentication
29+
30+
31+
# def cognito_authenticated(func):
32+
# @wraps(func)
33+
# @api_view(["GET", "POST", "PUT", "DELETE"])
34+
# @authentication_classes([CognitoAuthentication])
35+
# def wrapper(request, *args, **kwargs):
36+
# if not request.user or not hasattr(request.user, "get"):
37+
# return Response(
38+
# {"error": "Authentication credentials were not provided or are invalid."},
39+
# status=status.HTTP_401_UNAUTHORIZED,
40+
# )
41+
# return func(request, *args, **kwargs)
42+
43+
# return wrapper

Diff for: budget/admin.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from django.contrib import admin
33
from .models import Budget
44

5+
56
@admin.register(Budget)
67
class BudgetAdmin(admin.ModelAdmin):
7-
list_display = ('user', 'amount', 'created_at', 'updated_at')
8-
search_fields = ('user__username', 'amount')
8+
list_display = ("username", "amount", "created_at", "updated_at")
9+
search_fields = ("username", "amount")

Diff for: budget/migrations/0001_initial.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1-
# Generated by Django 5.0.6 on 2024-06-10 00:41
1+
# Generated by Django 5.0.6 on 2024-06-10 15:06
22

3-
import django.db.models.deletion
4-
from django.conf import settings
3+
import uuid
54
from django.db import migrations, models
65

76

87
class Migration(migrations.Migration):
98

109
initial = True
1110

12-
dependencies = [
13-
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14-
]
11+
dependencies = []
1512

1613
operations = [
1714
migrations.CreateModel(
1815
name="Budget",
1916
fields=[
20-
("id", models.AutoField(primary_key=True, serialize=False)),
17+
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
18+
("username", models.UUIDField()),
2119
("amount", models.IntegerField()),
2220
("created_at", models.DateTimeField(auto_now_add=True)),
2321
("updated_at", models.DateTimeField(auto_now=True)),
24-
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
2522
],
2623
),
2724
]

Diff for: budget/models.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# budget/models.py
22
from django.db import models
33
from django.conf import settings
4+
import uuid
45

56

67
class Budget(models.Model):
7-
id = models.AutoField(primary_key=True)
8-
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
8+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
9+
username = models.UUIDField()
910
amount = models.IntegerField()
1011
created_at = models.DateTimeField(auto_now_add=True)
1112
updated_at = models.DateTimeField(auto_now=True)

Diff for: budget/serializers.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from rest_framework import serializers
2+
from .models import Budget
3+
4+
5+
class BudgetSerializer(serializers.Serializer):
6+
username = serializers.UUIDField()
7+
amount = serializers.IntegerField()
8+
9+
def create(self, validated_data):
10+
return Budget.objects.create(**validated_data)

Diff for: budget/tests.py

+41-35
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,51 @@
1-
# budget/tests.py
2-
from django.test import TestCase, Client
3-
from django.urls import reverse
4-
from django.contrib.auth import get_user_model
1+
from django.test import TestCase
2+
from rest_framework.test import APIRequestFactory
3+
from .views import BudgetViewSet
54
from .models import Budget
6-
import json
5+
from django.contrib.auth.models import User
6+
from unittest.mock import patch
77

88

9-
class BudgetTests(TestCase):
9+
class BudgetViewSetTestCase(TestCase):
1010
def setUp(self):
11-
self.client = Client()
12-
self.user = get_user_model().objects.create_user(username="testuser", password="password")
11+
self.factory = APIRequestFactory()
12+
self.viewset = BudgetViewSet()
13+
self.user = User.objects.create_user(username="testuser", password="testpassword")
1314
self.budget = Budget.objects.create(user=self.user, amount=100)
1415

15-
def test_get_user_budget(self):
16-
url = reverse("get_user_budget")
17-
response = self.client.get(url, {"user_id": self.user.id}, content_type="application/json")
16+
@patch("budget.views.get_user_id_from_token")
17+
def test_list(self, mock_get_user_id_from_token):
18+
mock_get_user_id_from_token.return_value = self.user.id
19+
20+
request = self.factory.get("/")
21+
response = self.viewset.list(request)
1822
self.assertEqual(response.status_code, 200)
19-
self.assertEqual(response.json(), {"user_id": self.user.id, "amount": self.budget.amount})
20-
21-
def test_set_user_budget(self):
22-
url = reverse("set_user_budget")
23-
user = get_user_model().objects.create_user(username="testuser2", password="password")
24-
response = self.client.post(
25-
url, json.dumps({"user_id": user.id, "amount": 200}), content_type="application/json"
26-
)
23+
24+
@patch("budget.views.get_user_id_from_token")
25+
def test_create(self, mock_get_user_id_from_token):
26+
mock_get_user_id_from_token.return_value = self.user.id
27+
28+
request_data = {"amount": 200}
29+
request = self.factory.post("/", request_data, format="json")
30+
response = self.viewset.create(request)
2731
self.assertEqual(response.status_code, 201)
28-
self.assertEqual(response.json(), {"message": "Budget created successfully."})
29-
self.assertEqual(Budget.objects.get(user=user).amount, 200)
32+
self.assertEqual(Budget.objects.count(), 2)
3033

31-
def test_delete_user_budget(self):
32-
url = reverse("delete_user_budget")
33-
response = self.client.delete(url, json.dumps({"user_id": self.user.id}), content_type="application/json")
34-
self.assertEqual(response.status_code, 200)
35-
self.assertEqual(response.json(), {"message": "Budget deleted successfully", "user_id": self.user.id})
36-
self.assertFalse(Budget.objects.filter(user=self.user).exists())
37-
38-
def test_update_user_budget(self):
39-
url = reverse("update_user_budget")
40-
response = self.client.patch(
41-
url, json.dumps({"user_id": self.user.id, "amount": 300}), content_type="application/json"
42-
)
34+
@patch("budget.views.get_user_id_from_token")
35+
def test_destroy(self, mock_get_user_id_from_token):
36+
mock_get_user_id_from_token.return_value = self.user.id
37+
38+
request = self.factory.delete("/")
39+
response = self.viewset.destroy(request, pk=self.budget.id)
40+
self.assertEqual(response.status_code, 204)
41+
self.assertEqual(Budget.objects.count(), 0)
42+
43+
@patch("budget.views.get_user_id_from_token")
44+
def test_partial_update(self, mock_get_user_id_from_token):
45+
mock_get_user_id_from_token.return_value = self.user.id
46+
47+
request_data = {"amount": 150}
48+
request = self.factory.patch("/", request_data, format="json")
49+
response = self.viewset.partial_update(request, pk=self.budget.id)
4350
self.assertEqual(response.status_code, 200)
44-
self.assertEqual(response.json(), {"user_id": self.user.id, "budget": 300})
45-
self.assertEqual(Budget.objects.get(user=self.user).amount, 300)
51+
self.assertEqual(Budget.objects.get(pk=self.budget.id).amount, 150)

Diff for: budget/urls.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# budget/urls.py
22
from django.urls import path
3-
from .views import get_user_budget, set_user_budget, delete_user_budget, update_user_budget
3+
from .views import BudgetViewSet
44

55
urlpatterns = [
6-
path("get", get_user_budget, name="get_user_budget"),
7-
path("set", set_user_budget, name="set_user_budget"),
8-
path("delete", delete_user_budget, name="delete_user_budget"),
9-
path("update", update_user_budget, name="update_user_budget"),
6+
path(
7+
"",
8+
BudgetViewSet.as_view({"get": "list", "post": "create", "delete": "destroy", "put": "partial_update"}),
9+
name="budget",
10+
),
1011
]

Diff for: budget/views.py

+65-68
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,82 @@
1-
# budget/views.py
2-
from django.shortcuts import render
3-
from django.http import JsonResponse
1+
from rest_framework import viewsets, status
2+
from rest_framework.response import Response
43
from .models import Budget
5-
from django.views.decorators.csrf import csrf_exempt
6-
import json
4+
from .serializers import BudgetSerializer
5+
from authentication.decorators import cognito_authenticated
6+
import jwt
77

88

9-
@csrf_exempt
10-
def get_user_budget(request):
11-
if request.method == "GET":
9+
class BudgetViewSet(viewsets.ViewSet):
10+
def get_user_id_from_token(self, request):
1211
try:
13-
user_id = request.GET.get("user_id")
12+
authorization_header = request.headers.get("Authorization")
13+
if not authorization_header:
14+
raise Exception("Authorization header not found")
15+
16+
token = authorization_header.split()[1]
17+
decoded_token = jwt.decode(token, options={"verify_signature": False})
18+
user_id = decoded_token.get("username")
1419
if not user_id:
15-
return JsonResponse({"error": "User ID is required in the query parameters."}, status=400)
20+
raise Exception("User ID not found in token")
21+
return user_id
22+
except jwt.DecodeError:
23+
raise Exception("Invalid token")
24+
except jwt.ExpiredSignatureError:
25+
raise Exception("Expired token")
26+
except Exception as e:
27+
raise Exception(f"Error decoding token: {e}")
1628

17-
budget = Budget.objects.get(user_id=user_id)
18-
return JsonResponse({"user_id": int(user_id), "amount": budget.amount})
29+
@cognito_authenticated
30+
def list(self, request):
31+
try:
32+
username = self.get_user_id_from_token(request)
33+
budget = Budget.objects.get(username=username)
34+
serializer = BudgetSerializer(budget)
35+
return Response(serializer.data)
1936
except Budget.DoesNotExist:
20-
return JsonResponse({"error": "Budget not found"}, status=404)
21-
else:
22-
return JsonResponse({"error": "Invalid request method."}, status=405)
23-
37+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
38+
except Exception as e:
39+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
2440

25-
@csrf_exempt
26-
def set_user_budget(request):
27-
if request.method == "POST":
41+
@cognito_authenticated
42+
def create(self, request):
2843
try:
29-
data = json.loads(request.body)
30-
user_id = data.get("user_id")
31-
amount = data.get("amount")
32-
33-
if not user_id or not amount:
34-
return JsonResponse({"error": "User ID and amount are required."}, status=400)
35-
36-
budget = Budget.objects.create(user_id=user_id, amount=amount)
37-
budget.save()
38-
39-
return JsonResponse({"message": "Budget created successfully."}, status=201)
40-
except json.JSONDecodeError:
41-
return JsonResponse({"error": "Invalid JSON."}, status=400)
44+
user_id = self.get_user_id_from_token(request)
45+
data = request.data.copy()
46+
data["username"] = user_id
47+
48+
serializer = BudgetSerializer(data=data)
49+
print(serializer)
50+
if serializer.is_valid():
51+
serializer.save()
52+
return Response(serializer.data, status=status.HTTP_201_CREATED)
53+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
4254
except Exception as e:
43-
return JsonResponse({"error": str(e)}, status=500)
44-
else:
45-
return JsonResponse({"error": "Invalid request method."}, status=405)
55+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
4656

47-
48-
@csrf_exempt
49-
def delete_user_budget(request):
50-
if request.method == "DELETE":
57+
@cognito_authenticated
58+
def destroy(self, request):
5159
try:
52-
data = json.loads(request.body)
53-
user_id = data.get("user_id")
54-
55-
budget = Budget.objects.get(user_id=user_id)
60+
username = self.get_user_id_from_token(request)
61+
budget = Budget.objects.get(username=username)
5662
budget.delete()
57-
58-
return JsonResponse({"message": "Budget deleted successfully", "user_id": user_id})
63+
return Response(status=status.HTTP_204_NO_CONTENT)
5964
except Budget.DoesNotExist:
60-
return JsonResponse({"error": "Budget not found"}, status=404)
61-
except json.JSONDecodeError:
62-
return JsonResponse({"error": "Invalid JSON data"}, status=400)
63-
else:
64-
return JsonResponse({"error": "Invalid request method."}, status=405)
65-
65+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
66+
except Exception as e:
67+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
6668

67-
@csrf_exempt
68-
def update_user_budget(request):
69-
if request.method == "PATCH":
69+
@cognito_authenticated
70+
def partial_update(self, request):
7071
try:
71-
data = json.loads(request.body)
72-
user_id = data.get("user_id")
73-
amount = data.get("amount")
74-
75-
budget = Budget.objects.get(user_id=user_id)
76-
budget.amount = amount
77-
budget.save()
78-
79-
return JsonResponse({"user_id": user_id, "budget": budget.amount})
72+
username = self.get_user_id_from_token(request)
73+
budget = Budget.objects.get(username=username)
74+
serializer = BudgetSerializer(budget, data=request.data, partial=True)
75+
if serializer.is_valid():
76+
serializer.save()
77+
return Response(serializer.data)
78+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
8079
except Budget.DoesNotExist:
81-
return JsonResponse({"error": "Budget not found"}, status=404)
82-
except json.JSONDecodeError:
83-
return JsonResponse({"error": "Invalid JSON data"}, status=400)
84-
else:
85-
return JsonResponse({"error": "Invalid request method."}, status=405)
80+
return Response({"error": "Budget not found"}, status=status.HTTP_404_NOT_FOUND)
81+
except Exception as e:
82+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

0 commit comments

Comments
 (0)