Skip to content

Commit

Permalink
165305756-ft(Password Reset):User should be able to reset password vi…
Browse files Browse the repository at this point in the history
…a email

- Add password reset endpoint
- Add password reset confirm endpoint
- Update Readme
[Finishes #165305756]
  • Loading branch information
Ogutu-Brian committed Apr 30, 2019
1 parent df2b233 commit 9467a40
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 1 deletion.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,38 @@ Authentication required, returns the User

Accepted fields: `email``username``password``image``bio`

### Password Reset
`POST /api/users/password/reset/`

Example request body:

```source-json
{
"user":{
"email":"codingbrian58@gmail.com"
}
}
```

No authentication required, returns a token

Required fields: `email`

### Password Reset Confirm
`POST /api/users/password/reset/confirm/`

Example request body:

```source-json
{
"token":"bb57bb7c779d2c7872c8621d5735e3b8170d6105",
"password":"passroneggne2424",
"password_confirm":"passroneggne2424"
}
```

No authentication required, returns a success message

### Get Profile

`GET /api/profiles/:username`
Expand Down
55 changes: 55 additions & 0 deletions authors/apps/authentication/tests/basetests.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,58 @@ def get_user(self):
A method to retrieve users from the database
"""
return self.client.get(self.get_url)


class PasswordResetBaseTest(BaseTest):
"""
Base test for testing passeord reset
"""

def setUp(self):
super().setUp()
self.client = APIClient()
self.email = self.user.email
self.password_reset_url = reverse("authentication:password_reset")
self.token = None
self.password_reset_confirm_url = reverse(
"authentication:password_reset_confirm")
self.reset_data = {
"user": {
"email": self.email
}
}
self.password_data = {
"token": self.token,
"password": "HenkDTestPAss!#",
"password_confirm": "HenkDTestPAss!#"
}
self.contains_error = lambda container, error: error in container

def password_reset(self):
"""
Verifies user account and generates reset password
token
"""
response = self.client.post(
path=self.password_reset_url,
data=self.reset_data,
format="json"
)
if response.data.get("data"):
for item in response.data.get("data"):
if item.get("token"):
self.token = item.get("token")
break
self.password_data["token"] = self.token
return response

def password_reset_confirm(self):
"""
Confirms password reset by posting new password
"""
response = self.client.post(
path=self.password_reset_confirm_url,
data=self.password_data,
format="json"
)
return response
204 changes: 204 additions & 0 deletions authors/apps/authentication/tests/test_password_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
from .basetests import PasswordResetBaseTest
from rest_framework import status


class TestPasswordReset(PasswordResetBaseTest):
"""
Tests password reset by user
"""

def test_invalid_email_address(self):
"""
Tests posting of invalid email address
"""
self.reset_data["user"]["email"] = "johnsoon.com"
response = self.password_reset()
self.assertEqual(
response.status_code,
status.HTTP_400_BAD_REQUEST
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("email"),
"Enter a valid email address."
), True
)

def test_missing_email(self):
"""
Tests password reset without a mail
"""
self.reset_data["user"]["email"] = None
response = self.password_reset()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("email"),
"this field is required"
), True
)

def test_unexisting_ccount(self):
"""
Tests unexisting account
"""
self.reset_data["user"]["email"] = "jeff@gmail.com"
response = self.password_reset()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("email"),
"no account with that email address"
), True
)

def test_unmatching_password(self):
"""
Tests if passwords match
"""
self.password_data["password_confirm"] = "SomeTestPassword#!2"
self.password_reset()
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("password"),
"passwords did not match"
), True
)

def test_invalid_password(self):
"""
Tests invalid password
"""
self.password_data["password"] = "123"
self.password_data["password_confirm"] = "123"
self.password_reset()
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("password"),
"This password is too short. It must contain at least 8 characters."
), True
)

def test_invalid_token(self):
"""
Tests changing of password with invalid token
"""
self.password_data["token"] = "abcd898adwhi3454asddwhfwh"
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_401_UNAUTHORIZED
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("token"),
"invalid token"
), True
)

def test_missing_token(self):
"""
Tests password reset without a token
"""
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("token"),
"this field is required"
), True
)

def test_missing_password(self):
"""
Tests missing password
"""
self.password_data["password"] = None
self.password_reset()
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("password"),
"this field is required"
), True
)

def test_missing_password_confirm(self):
"""
Tests missing password confirm
"""
self.password_data["password_confirm"] = None
self.password_reset()
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_406_NOT_ACCEPTABLE
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("password_confirm"),
"this field is required"
), True
)

def test_successful_password_reset(self):
"""
Tests successful password rese
"""
self.password_reset()
response = self.password_reset_confirm()
message = None
for item in response.data.get("data"):
if item.get("message"):
message = item.get("message")
break
self.assertEqual(
response.status_code,
status.HTTP_200_OK
)
self.assertEqual(
message,
"you have successfully reset your password"
)

def test_token_reuse(self):
"""
Tests if a user can use token generated more than once to
reset password
"""
self.password_reset()
self.password_reset_confirm()
response = self.password_reset_confirm()
self.assertEqual(
response.status_code,
status.HTTP_401_UNAUTHORIZED
)
self.assertEqual(
self.contains_error(
response.data.get("errors").get("token"),
"invalid token"
), True
)
6 changes: 5 additions & 1 deletion authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.conf.urls import url

from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView, PasswordResetView, PasswordResetConfirmView
)

app_name = 'authentication'
Expand All @@ -10,4 +10,8 @@
url(r'^user/?$', UserRetrieveUpdateAPIView.as_view(), name='update_get'),
url(r'^users/?$', RegistrationAPIView.as_view(), name='registration'),
url(r'^users/login/?$', LoginAPIView.as_view(), name='login'),
url(r'^users/password/reset/?$',
PasswordResetView.as_view(), name='password_reset'),
url(r'^users/password/reset/confirm/?$',
PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
]
Loading

0 comments on commit 9467a40

Please sign in to comment.