diseño, creación e implementación de user API
serializadores, administración, autenticación y tests
This commit is contained in:
parent
d035ec4cca
commit
f8ac495c2d
545
README.md
545
README.md
@ -1309,3 +1309,548 @@ class UserAdmin(BaseUserAdmin):
|
||||
|
||||
- Página para crear usuarios ![img](./imgs_readme/django_admin_12.png)
|
||||
- Panel de usuarios del administrador ![img](./imgs_readme/django_admin_13.png)
|
||||
|
||||
## Documentación de la API
|
||||
|
||||
Es necesario tener acceso a una buena documentación para que los desarrolladores
|
||||
puedan saber como usarla. Se documenta todo lo que sea necesario para usar la API
|
||||
|
||||
- Endopoints disponibles (paths)
|
||||
- Métodos soportados `GET`, `POST`, `PUT`, `PATCH`, `DELETE`...
|
||||
- Formateo de payloads (inputs). Parametros, Post en formato **JSON**
|
||||
- Formateo de respuestas (outputs). Respuesta en formato **JSON**
|
||||
- Proceso Autenticación
|
||||
|
||||
### Opiones de documentación
|
||||
|
||||
- Manual
|
||||
- Documento de texto
|
||||
- Markdown
|
||||
- Automatizada
|
||||
- Usa la metadata del código (comments)
|
||||
- Genera páginas de documentación
|
||||
|
||||
## Autodocs de DRF
|
||||
|
||||
- Documentación Autogenerada (3rd party library)
|
||||
- `drf-spectacular`
|
||||
- Genera el "schema"
|
||||
- Interfaz web navegable
|
||||
- Test requests
|
||||
- Maneja la autenticación
|
||||
|
||||
### Como funciona
|
||||
|
||||
1. Creación del archivo `schema`
|
||||
2. Pasa el schema al GUI
|
||||
|
||||
### Open API Schema
|
||||
|
||||
- Estandar para describir APIs
|
||||
- Popular en la industria
|
||||
- Soportada por la mayoría de herramientas de documentación de API
|
||||
|
||||
### Ejemplo Schema
|
||||
|
||||
fragmento
|
||||
|
||||
```yml
|
||||
/api/recipe/ingredients/:
|
||||
get:
|
||||
oprationId: recipe_ingredients_list
|
||||
description: Manage ingredients in the database.
|
||||
parameteres:
|
||||
- in: query
|
||||
name: assigned_only
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
description: Filter by item assigned to recipies
|
||||
tags:
|
||||
- recipe
|
||||
security:
|
||||
- tokenAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Ingredient'
|
||||
description: ''
|
||||
...
|
||||
```
|
||||
|
||||
### Implementación DRF
|
||||
|
||||
Se agrega dependencia en `drf-spectacular>=0.16` en `requirements.txt`
|
||||
|
||||
Instalar app, en `settings.py`
|
||||
|
||||
```py
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'core',
|
||||
'rest_framework',
|
||||
'drf_spectacular',
|
||||
]
|
||||
|
||||
...
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
}
|
||||
```
|
||||
|
||||
### Activar las URLS
|
||||
|
||||
```py
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
|
||||
path(
|
||||
'api/docs',
|
||||
SpectacularSwaggerView.as_view(url_name='api-schema'),
|
||||
name='api-docs'),
|
||||
]
|
||||
```
|
||||
|
||||
- `docker compose run`
|
||||
- `127.0.0.1:8000/api/docs`
|
||||
|
||||
## User API
|
||||
|
||||
### Diseño
|
||||
|
||||
- Registro de usario
|
||||
- Creación de token de autenticación
|
||||
- Consultar y actualizar perfil
|
||||
|
||||
### Endpoins
|
||||
|
||||
| Endpoint | Method | Descripción |
|
||||
| - | - | - |
|
||||
| `user/create` | `POST` | Registrar nuevo usuario |
|
||||
| `user/token` | `POST` | Crea un nuevo token |
|
||||
| `user/me/` | `PUT/PATCH` | Actualizar perfíl |
|
||||
|
||||
### Creación user app
|
||||
|
||||
`docker compose run --rm app sh -c "python manage.py startapp user"`
|
||||
|
||||
```sh
|
||||
[+] Creating 1/0
|
||||
✔ Container recipes_api_django-db-1 Created 0.0s
|
||||
[+] Running 1/1
|
||||
✔ Container recipes_api_django-db-1 Started 0.2s
|
||||
```
|
||||
|
||||
Activar `user` app en `settings.py`
|
||||
|
||||
### Test User API
|
||||
|
||||
[`app/user/tests/test_user_api.py`](./app/user/tests/test_user_api.py)
|
||||
|
||||
```py
|
||||
CREATE_USER_URL = reverse('user:create')
|
||||
|
||||
def create_user(**params):
|
||||
"""Create and return a new user."""
|
||||
return get_user_model().objects.crate_user(**params)
|
||||
|
||||
|
||||
class PublicUserApiTest(TestCase):
|
||||
"""Test the public features of the user API."""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def test_create_user_success(self):
|
||||
"""Tests creating a user is successful."""
|
||||
payload = {
|
||||
'email': 'test@example.com',
|
||||
'password':'testpass123',
|
||||
'name': 'TestName',
|
||||
}
|
||||
res = self.client.post(CREATE_USER_URL, payload)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
|
||||
user = get_user_model().objects.get(email=payload['email'])
|
||||
self.assertTrue(user.check_password(payload['password']))
|
||||
self.assertNotIn('password', res.data)
|
||||
|
||||
def test_user_with_email_exists_error(self):
|
||||
"""Test error returned if user with email exists."""
|
||||
payload = {
|
||||
'email': 'test@example.com',
|
||||
'password':'testpass123',
|
||||
'name': 'Test Name',
|
||||
}
|
||||
create_user(**payload)
|
||||
res = self.client.post(CREATE_USER_URL, payload)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_password_too_short_error(self):
|
||||
"""Test an error is returned if password less than 5 chars."""
|
||||
payload = {
|
||||
'email': 'test@example.com',
|
||||
'password':'pw',
|
||||
'name': 'Test Name',
|
||||
}
|
||||
res = self.client.post(CREATE_USER_URL, payload)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
user_exists = get_user_model().objects.filter(
|
||||
email=payload['email']
|
||||
).exists()
|
||||
self.assertFalse(user_exists)
|
||||
```
|
||||
|
||||
### Creando funcionalidad de User API
|
||||
|
||||
- [serializers.py](./app/user/serializers.py)
|
||||
|
||||
```py
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import serializers
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Seralizer for the model object."""
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ['email', 'password', 'name']
|
||||
extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create and return a user with encrypted password"""
|
||||
return get_user_model().objects.create_user(**validated_data)
|
||||
```
|
||||
|
||||
- [views.py](./app/user/views.py)
|
||||
|
||||
```py
|
||||
from rest_framework import generics
|
||||
from user.serializers import UserSerializer
|
||||
|
||||
class CreateUserView(generics.CreateAPIView):
|
||||
"""Create a new user in the system."""
|
||||
serializer_class = UserSerializer
|
||||
```
|
||||
|
||||
- [urls.py](./app/user/urls.py)
|
||||
|
||||
```py
|
||||
from django.urls import path
|
||||
from user import views
|
||||
|
||||
app_name='user'
|
||||
urlpatterns = [
|
||||
path('create/', views.CreateUserView.as_view(), name='create')
|
||||
]
|
||||
```
|
||||
|
||||
- [app/urls.py](./app/app/urls.py)
|
||||
|
||||
```py
|
||||
...
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
...
|
||||
path('api/user/', include('user.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
### Autenticación
|
||||
|
||||
| Tipo de autenticación | Descripción |
|
||||
| - | - |
|
||||
| **Básica** | Envía usuario y password en cada request |
|
||||
| **Token** | Usa un token en el encabezado HTTP |
|
||||
| **JSON Web Token (JWT)** | Usa un token de acceso |
|
||||
| **Sesión** | Usa cookies |
|
||||
|
||||
En esta app se utilza **Token** por:
|
||||
|
||||
- Balance entre simplicidad y seguridad
|
||||
- Soporte por defecto por **DRF**
|
||||
- Bién soportada por la mayoria de clientes
|
||||
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
|
||||
flowchart
|
||||
subgraph " "
|
||||
|
||||
CT["<b>Create token</b>
|
||||
(Post username/password)"]
|
||||
STOC["<b>Store token on client</b>"]
|
||||
ITIH["<b>Include token in HTTP headers</b>"]
|
||||
CT .-> STOC .-> ITIH
|
||||
end
|
||||
```
|
||||
|
||||
#### Pros del uso de Token
|
||||
|
||||
- Soporte por defecto
|
||||
- Simple de usar
|
||||
- Soportada por todos los clientes
|
||||
- Evita enviar datos de usuario/password en cada request
|
||||
|
||||
#### Cons del uso de Token
|
||||
|
||||
- El token debe ser seguro
|
||||
- Requiere hacer peticiones a la base de datos
|
||||
|
||||
### Login out
|
||||
|
||||
- Sucede en el lado del cliente
|
||||
- Borra el token
|
||||
|
||||
### Test token API
|
||||
|
||||
Agregar tests en
|
||||
[`app/user/tests/test_user_api.py`](./app/user/tests/test_user_api.py)
|
||||
|
||||
```py
|
||||
...
|
||||
TOKEN_URL = reverse('user:token')
|
||||
|
||||
...
|
||||
|
||||
class PublicUserApiTest(TestCase):
|
||||
"""Test the public features of the user API."""
|
||||
|
||||
...
|
||||
|
||||
def test_create_token_for_user(self):
|
||||
"""Test generate token for valid credentials."""
|
||||
user_details = {
|
||||
'name': 'Test Name',
|
||||
'email': 'test@example.com',
|
||||
'password':'test-user-password123',
|
||||
}
|
||||
create_user(**user_details)
|
||||
|
||||
payload = {
|
||||
'email': user_details['email'],
|
||||
'password': user_details['password'],
|
||||
}
|
||||
res = self.client.post(TOKEN_URL, payload)
|
||||
|
||||
self.assertIn('token', res.data)
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_create_token_bad_credentials(self):
|
||||
"""Test returns error if credentials invalid."""
|
||||
create_user(email='test@example.com', password='goodpass')
|
||||
|
||||
payload = {'email': 'test@example.com' ,'password': 'badpass'}
|
||||
res = self.client.post(TOKEN_URL, payload)
|
||||
|
||||
self.assertNotIn('token', res.data)
|
||||
self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_create_token_blank_password(self):
|
||||
"""Test posting a blank password returns an error."""
|
||||
payload = {'email': 'test@example.com' , 'password': ''}
|
||||
res = self.client.post(TOKEN_URL, payload)
|
||||
|
||||
self.assertNotIn('token', res.data)
|
||||
self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
```
|
||||
|
||||
### Implementar Token API
|
||||
|
||||
- Añadir app `rest_framework.authtoken` en [settings.py](./app/app/settings.py)
|
||||
|
||||
```py
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken', # <---
|
||||
'drf_spectacular',
|
||||
'user',
|
||||
]
|
||||
```
|
||||
|
||||
### Creación del serlizador para token api
|
||||
|
||||
- [user/serializer.py](./app/user/serializers.py)
|
||||
|
||||
```py
|
||||
...
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
"""Serializer for the user auth token."""
|
||||
email = serializer.EmailField()
|
||||
password = serializer.CharField(
|
||||
style={'input_type': 'password'},
|
||||
trim_whitespace=False,
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate and authenticate the user."""
|
||||
email = attrs.get('email')
|
||||
password = attrs.get('password')
|
||||
user = authenticate(
|
||||
request=self.context.get('request'),
|
||||
username=email,
|
||||
password=password,
|
||||
)
|
||||
if not user:
|
||||
msg = _('Unable to authenticate with provided credentials.')
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
```
|
||||
|
||||
- vista [user/views.py](./app/user/views.py)
|
||||
|
||||
```py
|
||||
...
|
||||
class CreateTokenView(ObtainAuthToken):
|
||||
"""Create a new auth token for user."""
|
||||
serializer_class = AuthTokenSerializer
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
```
|
||||
|
||||
- urls [user/urls.py](./app/user/urls.py)
|
||||
|
||||
```py
|
||||
urlpatterns = [
|
||||
...
|
||||
path('token/', views.CreateTokenView.as_view(), name='token'),
|
||||
]
|
||||
```
|
||||
|
||||
### Test administrar usuario
|
||||
|
||||
- [test_user_api.py](./app/user/tests/test_user_api.py)
|
||||
|
||||
```py
|
||||
...
|
||||
ME_URL = reverse('user:me')
|
||||
|
||||
...
|
||||
|
||||
class PrivateUserApiTests(TestCase):
|
||||
"""Test API requests that require authentication."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_user(
|
||||
email='test@example.com',
|
||||
password='testpass123',
|
||||
name='Test Name',
|
||||
)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_retrive_profile_success(self):
|
||||
"""Test retrieving profile for logged in user."""
|
||||
res = self.client.get(ME_URL)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
res.data, {
|
||||
'name': self.user.name,
|
||||
'email': self.user.email,
|
||||
})
|
||||
|
||||
def test_post_me_not_allowed(self):
|
||||
"""Test POST is not allowed for the 'me' endpoint."""
|
||||
res = self.client.post(ME_URL, {})
|
||||
|
||||
self.assertAlmostEqual(
|
||||
res.status_code,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
)
|
||||
|
||||
def test_update_user_profile(self):
|
||||
"""Test updating the user profile for the autenticated user."""
|
||||
payload = { 'name': 'Updated Name', 'password': 'newpassword123' }
|
||||
|
||||
res = self.client.patch(ME_URL, payload)
|
||||
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.name, payload['name'])
|
||||
self.assertTrue(self.user.check_password(payload['password']))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
```
|
||||
|
||||
### Implementación API actualizar usuario
|
||||
|
||||
`me` endpoint
|
||||
|
||||
- creación (sobrescritura) del método update
|
||||
[serializer.py](./app/user/serializers.py)
|
||||
|
||||
```py
|
||||
...
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
...
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create and return a user with encrypted password"""
|
||||
return get_user_model().objects.create_user(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update and return user."""
|
||||
password = validated_data.pop('password', None)
|
||||
user = super().update(instance, validated_data)
|
||||
|
||||
if password:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
- vistas [views.py](./app/user/views.py)
|
||||
|
||||
```py
|
||||
from rest_framework import generics, authentication, permissions
|
||||
...
|
||||
|
||||
class ManageUserView(generics.RetrieveUpdateAPIView):
|
||||
"""Manage the autenticated user."""
|
||||
serializer_class = UserSerializer
|
||||
authentication_classes = [authentication.TokenAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_object(self):
|
||||
"""Retrieve and return the authenticated user."""
|
||||
return self.request.user
|
||||
```
|
||||
|
||||
- urls [urls.py](./app/user/urls.py)
|
||||
|
||||
```py
|
||||
urlpatterns = [
|
||||
...
|
||||
path('me/', views.ManageUserView.as_view(), name='me'),
|
||||
]
|
||||
```
|
||||
|
||||
### Pruebas en navegador
|
||||
|
||||
Ruta `localhost:8000/api/docs`
|
||||
|
||||
![img](./imgs_readme/api_swagger_00.png)
|
||||
|
||||
----
|
||||
|
||||
Segunda parte -> [Recetas](./README2.md)
|
||||
|
@ -38,6 +38,10 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'core',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'drf_spectacular',
|
||||
'user',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -127,3 +131,7 @@ STATIC_URL = 'static/'
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
AUTH_USER_MODEL = 'core.User'
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
}
|
||||
|
@ -14,9 +14,16 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
|
||||
path(
|
||||
'api/docs',
|
||||
SpectacularSwaggerView.as_view(url_name='api-schema'),
|
||||
name='api-docs'),
|
||||
path('api/user/', include('user.urls')),
|
||||
]
|
||||
|
@ -2,7 +2,7 @@
|
||||
Test for models.
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user, get_user_model
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
||||
class ModelTests(TestCase):
|
||||
|
0
app/user/__init__.py
Normal file
0
app/user/__init__.py
Normal file
6
app/user/apps.py
Normal file
6
app/user/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UserConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'user'
|
56
app/user/serializers.py
Normal file
56
app/user/serializers.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""
|
||||
Seralizers for the user API View
|
||||
"""
|
||||
from django.contrib.auth import get_user_model, authenticate
|
||||
from django.utils.translation import gettext as _, trim_whitespace
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Seralizer for the model object."""
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ['email', 'password', 'name']
|
||||
extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create and return a user with encrypted password"""
|
||||
return get_user_model().objects.create_user(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update and return user."""
|
||||
password = validated_data.pop('password', None)
|
||||
user = super().update(instance, validated_data)
|
||||
|
||||
if password:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
"""Serializer for the user auth token."""
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField(
|
||||
style={'input_type': 'password'},
|
||||
trim_whitespace=False,
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate and authenticate the user."""
|
||||
email = attrs.get('email')
|
||||
password = attrs.get('password')
|
||||
user = authenticate(
|
||||
request=self.context.get('request'),
|
||||
username=email,
|
||||
password=password,
|
||||
)
|
||||
if not user:
|
||||
msg = _('Unable to authenticate with provided credentials.')
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
|
||||
attrs['user'] = user
|
||||
return attrs
|
0
app/user/tests/__init__.py
Normal file
0
app/user/tests/__init__.py
Normal file
151
app/user/tests/test_user_api.py
Normal file
151
app/user/tests/test_user_api.py
Normal file
@ -0,0 +1,151 @@
|
||||
"""
|
||||
Tests for the user API.
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
|
||||
CREATE_USER_URL = reverse('user:create')
|
||||
TOKEN_URL = reverse('user:token')
|
||||
ME_URL = reverse('user:me')
|
||||
|
||||
def create_user(**params):
|
||||
"""Create and return a new user."""
|
||||
return get_user_model().objects.create_user(**params)
|
||||
|
||||
|
||||
class PublicUserApiTest(TestCase):
|
||||
"""Test the public features of the user API."""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def test_create_user_success(self):
|
||||
"""Tests creating a user is successful."""
|
||||
payload = {
|
||||
'email': 'test@example.com',
|
||||
'password':'testpass123',
|
||||
'name': 'TestName',
|
||||
}
|
||||
res = self.client.post(CREATE_USER_URL, payload)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
|
||||
user = get_user_model().objects.get(email=payload['email'])
|
||||
self.assertTrue(user.check_password(payload['password']))
|
||||
self.assertNotIn('password', res.data)
|
||||
|
||||
def test_user_with_email_exists_error(self):
|
||||
"""Test error returned if user with email exists."""
|
||||
payload = {
|
||||
'email': 'test@example.com',
|
||||
'password':'testpass123',
|
||||
'name': 'Test Name',
|
||||
}
|
||||
create_user(**payload)
|
||||
res = self.client.post(CREATE_USER_URL, payload)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_password_too_short_error(self):
|
||||
"""Test an error is returned if password less than 5 chars."""
|
||||
payload = {
|
||||
'email': 'test@example.com',
|
||||
'password':'pw',
|
||||
'name': 'Test Name',
|
||||
}
|
||||
res = self.client.post(CREATE_USER_URL, payload)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
user_exists = get_user_model().objects.filter(
|
||||
email=payload['email']
|
||||
).exists()
|
||||
self.assertFalse(user_exists)
|
||||
|
||||
def test_create_token_for_user(self):
|
||||
"""Test generate token for valid credentials."""
|
||||
user_details = {
|
||||
'name': 'Test Name',
|
||||
'email': 'test@example.com',
|
||||
'password':'test-user-password123',
|
||||
}
|
||||
create_user(**user_details)
|
||||
|
||||
payload = {
|
||||
'email': user_details['email'],
|
||||
'password': user_details['password'],
|
||||
}
|
||||
res = self.client.post(TOKEN_URL, payload)
|
||||
|
||||
self.assertIn('token', res.data)
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_create_token_bad_credentials(self):
|
||||
"""Test returns error if credentials invalid."""
|
||||
create_user(email='test@example.com', password='goodpass')
|
||||
|
||||
payload = {'email': 'test@example.com' ,'password': 'badpass'}
|
||||
res = self.client.post(TOKEN_URL, payload)
|
||||
|
||||
self.assertNotIn('token', res.data)
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_create_token_blank_password(self):
|
||||
"""Test posting a blank password returns an error."""
|
||||
payload = {'email': 'test@example.com' , 'password': ''}
|
||||
res = self.client.post(TOKEN_URL, payload)
|
||||
|
||||
self.assertNotIn('token', res.data)
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_retrive_user_unauthorized(self):
|
||||
"""Test authentication is required for users."""
|
||||
res = self.client.get(ME_URL)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class PrivateUserApiTests(TestCase):
|
||||
"""Test API requests that require authentication."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_user(
|
||||
email='test@example.com',
|
||||
password='testpass123',
|
||||
name='Test Name',
|
||||
)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_retrive_profile_success(self):
|
||||
"""Test retrieving profile for logged in user."""
|
||||
res = self.client.get(ME_URL)
|
||||
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
res.data, {
|
||||
'name': self.user.name,
|
||||
'email': self.user.email,
|
||||
})
|
||||
|
||||
def test_post_me_not_allowed(self):
|
||||
"""Test POST is not allowed for the 'me' endpoint."""
|
||||
res = self.client.post(ME_URL, {})
|
||||
|
||||
self.assertAlmostEqual(
|
||||
res.status_code,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
)
|
||||
|
||||
def test_update_user_profile(self):
|
||||
"""Test updating the user profile for the autenticated user."""
|
||||
payload = { 'name': 'Updated Name', 'password': 'newpassword123' }
|
||||
|
||||
res = self.client.patch(ME_URL, payload)
|
||||
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.name, payload['name'])
|
||||
self.assertTrue(self.user.check_password(payload['password']))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
15
app/user/urls.py
Normal file
15
app/user/urls.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
URL mappings for the user API
|
||||
"""
|
||||
from django.urls import path
|
||||
|
||||
from user import views
|
||||
|
||||
|
||||
app_name='user'
|
||||
|
||||
urlpatterns = [
|
||||
path('create/', views.CreateUserView.as_view(), name='create'),
|
||||
path('token/', views.CreateTokenView.as_view(), name='token'),
|
||||
path('me/', views.ManageUserView.as_view(), name='me'),
|
||||
]
|
29
app/user/views.py
Normal file
29
app/user/views.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""
|
||||
Views for the user API.
|
||||
"""
|
||||
from rest_framework import generics, authentication, permissions
|
||||
from rest_framework.authtoken.views import ObtainAuthToken
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from user.serializers import UserSerializer, AuthTokenSerializer
|
||||
|
||||
|
||||
class CreateUserView(generics.CreateAPIView):
|
||||
"""Create a new user in the system."""
|
||||
serializer_class = UserSerializer
|
||||
|
||||
|
||||
class CreateTokenView(ObtainAuthToken):
|
||||
"""Create a new auth token for user."""
|
||||
serializer_class = AuthTokenSerializer
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
|
||||
class ManageUserView(generics.RetrieveUpdateAPIView):
|
||||
"""Manage the autenticated user."""
|
||||
serializer_class = UserSerializer
|
||||
authentication_classes = [authentication.TokenAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_object(self):
|
||||
"""Retrieve and return the authenticated user."""
|
||||
return self.request.user
|
BIN
imgs_readme/api_swagger_00.png
Normal file
BIN
imgs_readme/api_swagger_00.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -1,3 +1,4 @@
|
||||
Django==4.2.5
|
||||
djangorestframework==3.14.0
|
||||
psycopg2>=2.9.9
|
||||
drf-spectacular>=0.16
|
||||
|
Loading…
Reference in New Issue
Block a user