Skip to content

Commit 940fd5d

Browse files
committed
add documentation and tests
1 parent 7888dc9 commit 940fd5d

10 files changed

Lines changed: 152 additions & 15 deletions

File tree

documentation/docs/backend/authentication.md renamed to documentation/docs/backend/authentication/authentication.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This API is used to manage the authentication lifecycle. It allows us to:
1616
/api/auth
1717

1818
----
19+
1920
**Endpoints:**
2021

2122
## /register/
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Authentication REST API
2+
3+
----
4+
This API is used to manage the password reset lifecycle. It allows us to:
5+
6+
* Send a password reset email with an SMTP server (currently configured for gmail)
7+
* Verfiy that a token and associated uid is valid
8+
* Reset the user password using a valid token and associated uid
9+
10+
----
11+
**Base URL:**
12+
13+
/api/auth/reset
14+
15+
----
16+
17+
**Endpoints:**
18+
19+
## /
20+
21+
Methods: POST, PUT
22+
Description: PUT to veriy that a token/uid pair is valid. POST to change passwords.
23+
24+
**URL Parameters**
25+
26+
| Key | Data Type | Description |
27+
| :---- | :-------: | :----------------------------------------------------- |
28+
| token | String | A password reset token. |
29+
| uid | String | The url-safe base64 encoded email address of the user. |
30+
31+
**Data Parameters:**
32+
33+
| Key | Data Type | Description |
34+
| :------- | :-------: | :---------------- |
35+
| password | String | The new password. |
36+
37+
Example request
38+
39+
```json
40+
{
41+
"password": "123456",
42+
}
43+
```
44+
45+
Example response
46+
47+
```json
48+
// TODO
49+
```
50+
51+
## /email/
52+
53+
Methods: POST
54+
Description: Register an user. Returns the user JSON as the response.
55+
Success status code: 200 OK
56+
57+
**Data Parameters:**
58+
59+
| Key | Data Type | Description |
60+
| :---- | :-------: | :--------------------- |
61+
| email | String | The email of the user. |
File renamed without changes.

server/api/apps/auth/reset/migrations/__init__.py renamed to server/api/apps/auth/reset_password/migrations/__init__.py

File renamed without changes.

server/api/apps/auth/reset/serializers.py renamed to server/api/apps/auth/reset_password/serializers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33

44

55
class ChangePasswordSerializer(serializers.Serializer):
6-
email = serializers.CharField(required=True)
7-
new_password = serializers.CharField(required=True)
6+
password = serializers.CharField(required=True)
87

9-
def validate_new_password(self, value):
8+
def validate_password(self, value):
109
validate_password(value)
1110
return value
1211

File renamed without changes.

server/api/apps/auth/reset/views.py renamed to server/api/apps/auth/reset_password/views.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ def validate_token(self, request):
3535
is_valid_token = PasswordResetTokenGenerator().check_token(
3636
token=token, user=user
3737
)
38-
return is_valid_token
38+
return is_valid_token, user
3939

4040
def get(self, request, *args, **kwargs):
41-
if not self.validate_token(request):
41+
is_valid_token, user = self.validate_token(request)
42+
if not is_valid_token:
4243
return Response(
4344
{
4445
"message": (
@@ -54,7 +55,8 @@ def get(self, request, *args, **kwargs):
5455
)
5556

5657
def update(self, request, *args, **kwargs):
57-
if not self.validate_token(request):
58+
is_valid_token, user = self.validate_token(request)
59+
if not is_valid_token:
5860
return Response(
5961
{
6062
"message": (
@@ -71,19 +73,13 @@ def update(self, request, *args, **kwargs):
7173
serializer.errors, status=status.HTTP_400_BAD_REQUEST
7274
)
7375

74-
user = (
75-
get_user_model()
76-
.objects.filter(email=serializer.data.get("email"))
77-
.first()
78-
)
79-
8076
if not user:
8177
return Response(
8278
{"message": "Email does not exist"},
8379
status=status.HTTP_400_BAD_REQUEST,
8480
)
8581

86-
user.set_password(serializer.data.get("new_password"))
82+
user.set_password(serializer.data.get("password"))
8783
user.save()
8884

8985
return Response(

server/api/apps/auth/tests/test_register_user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from rest_framework.test import APITestCase
44

55

6-
class AuthTestCase(APITestCase):
6+
class RegisterTestCase(APITestCase):
77
def test_get_request(self):
88
"""
99
GIVEN: GET request to endpoint '/api/auth/register'
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from django.contrib.auth import get_user_model
2+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
3+
from django.urls import reverse
4+
from django.utils.encoding import force_bytes
5+
from django.utils.http import urlsafe_base64_encode
6+
from rest_framework import status
7+
from rest_framework.test import APITestCase
8+
9+
10+
class ResetPasswordTestCase(APITestCase):
11+
def setUp(self):
12+
self.testuser = get_user_model().objects.create_user(
13+
email="john.doe@example.com",
14+
first_name="John",
15+
last_name="Doe",
16+
grade="Grade 11",
17+
password="password",
18+
)
19+
self.testuser.save()
20+
21+
def test_send_reset_email(self):
22+
"""
23+
GIVEN: The endpoint '/api/auth/reset/email/' and an active user
24+
WHEN: A POST request is sent to the endpoint with the HTTP body that
25+
contains an email address corresponding to a valid account
26+
THEN: The API HTTP Response Code should be 200
27+
AND: The SMTP server will be called to send a password reset email to
28+
the specified address
29+
"""
30+
url = reverse("send-reset-email")
31+
body = {"email": "john.doe@example.com"}
32+
33+
response = self.client.post(url, body, format="json")
34+
35+
self.assertEqual(response.status_code, status.HTTP_200_OK)
36+
37+
def test_verify_reset_token(self):
38+
"""
39+
WHEN: A GET request is sent to the endpoint with uid and token as url
40+
parameters
41+
THEN: Returns 200 status code if the token and uid is valid
42+
"""
43+
44+
user = (
45+
get_user_model()
46+
.objects.filter(email="john.doe@example.com")
47+
.first()
48+
)
49+
token = PasswordResetTokenGenerator().make_token(user)
50+
uid = urlsafe_base64_encode(force_bytes(user.email))
51+
52+
reset_url = f"{reverse('reset')}?token={token}&uid={uid}/"
53+
54+
response = self.client.get(reset_url)
55+
56+
self.assertEqual(response.status_code, status.HTTP_200_OK)
57+
58+
def test_reset_password(self):
59+
"""
60+
GIVEN: The endpoint '/api/auth/reset/' and a valid uid and token
61+
WHEN: A PUT request is sent to the endpoint with uid and token as url
62+
parameters and password in JSON body
63+
THEN: Returns 200 status code if the password has been reset.
64+
"""
65+
66+
user = (
67+
get_user_model()
68+
.objects.filter(email="john.doe@example.com")
69+
.first()
70+
)
71+
token = PasswordResetTokenGenerator().make_token(user)
72+
uid = urlsafe_base64_encode(force_bytes(user.email))
73+
74+
reset_url = f"{reverse('reset')}?token={token}&uid={uid}/"
75+
76+
body = {"password": "123456"}
77+
78+
response = self.client.put(reset_url, body, format="json")
79+
80+
self.assertEqual(response.status_code, status.HTTP_200_OK)

server/api/apps/auth/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
path("refresh/", refresh_jwt_token, name="refresh-jwt-token"),
1313
path("verify/", verify_jwt_token, name="verify-jwt-token"),
1414
path("register/", RegistrationView.as_view(), name="register"),
15-
path("reset/", include("api.apps.auth.reset.urls")),
15+
path("reset/", include("api.apps.auth.reset_password.urls")),
1616
]

0 commit comments

Comments
 (0)