Prepare base structures for API' tests.
This commit is contained in:
parent
fc4930a49e
commit
f4041d6e02
4 changed files with 714 additions and 292 deletions
694
api/test/testcases.py
Normal file
694
api/test/testcases.py
Normal file
|
@ -0,0 +1,694 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class GenericAPITestCaseMixin:
|
||||||
|
"""
|
||||||
|
Base mixin for testing one or more operations on a model.
|
||||||
|
|
||||||
|
The specifics of each operation is not implemented here.
|
||||||
|
Each operation has its own TestCase, which relies on methods/attributes
|
||||||
|
defined here (some or all).
|
||||||
|
|
||||||
|
The "real" mixins for each operation are:
|
||||||
|
[List,Create,Retrieve,Update,Destroy]APITestCaseMixin.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
E.g. for a creation test, your testcase should be something like:
|
||||||
|
|
||||||
|
class MyModelCreateAPITestCase(
|
||||||
|
CreateAPITestCaseMixin,
|
||||||
|
GenericAPITestCaseMixin,
|
||||||
|
APITestCase,
|
||||||
|
):
|
||||||
|
...
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# General
|
||||||
|
model (models.Model): The model class under test.
|
||||||
|
url_label (str): Use to get the API urls of the model under test:
|
||||||
|
- The model url is:`reverse('{url_label}-list).
|
||||||
|
This is used by `[List,Create]APITestCaseMixin`.
|
||||||
|
The url can also be customized by defining `get_url_model`
|
||||||
|
method.
|
||||||
|
- The url of a model object is: `reverse('{label}-detail')`.
|
||||||
|
This is used by `[Retrieve,Update,Destroy]APITestCaseMixin`.
|
||||||
|
The url can also be customized by defining `get_url_object`
|
||||||
|
method.
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
user_auth (User instance): If given, this user is authenticated for
|
||||||
|
each request sent to the API. Default to `None`.
|
||||||
|
See section 'Authenticate requests'.
|
||||||
|
user_auth_mapping (dict of str: User instance): Associates a user to
|
||||||
|
authenticate for each type of API request.
|
||||||
|
See section 'Authenticate requests'.
|
||||||
|
|
||||||
|
Authenticate requests:
|
||||||
|
Each real testcase call `get_user` method with a label for the
|
||||||
|
operation under test (e.g. UpdateAPITestCaseMixin will call
|
||||||
|
`get_user('update')`. If it returns a user, this latter is
|
||||||
|
authenticated with `force_authenticate` method of the test client.
|
||||||
|
|
||||||
|
The default behavior, which can be customized by replacing `get_user`
|
||||||
|
method, is to return `user_auth` attribute/property if not None. Else,
|
||||||
|
`user_auth_mapping[operation_type_label]` or None if key is missing.
|
||||||
|
|
||||||
|
A basic user is created during setUp. It can be found as `user`
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
"""
|
||||||
|
model = None
|
||||||
|
url_label = ''
|
||||||
|
|
||||||
|
user_auth = None
|
||||||
|
user_auth_mapping = {}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Prepare a test execution.
|
||||||
|
|
||||||
|
Don't forget to call super().setUp() if you define this in subclass.
|
||||||
|
"""
|
||||||
|
# Dict comparison can be large.
|
||||||
|
self.maxDiff = 2000
|
||||||
|
|
||||||
|
# This will be returned by django.utils.timezone.now thanks to the mock
|
||||||
|
# in test methods.
|
||||||
|
# This can be useful to check auto_now and auto_now_add fields.
|
||||||
|
self.now = timezone.now()
|
||||||
|
|
||||||
|
# A very basic user.
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username='user', password='user',
|
||||||
|
first_name='first', last_name='last',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user(self, action):
|
||||||
|
"""
|
||||||
|
Returns a user to authenticate for requests of type 'action'.
|
||||||
|
|
||||||
|
Property `user_auth` has precedence over the property
|
||||||
|
`user_auth_mapping`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action (str): Operation label. See LCRUD TestCases for reserved
|
||||||
|
labels, before setting your own if you create a new common
|
||||||
|
operation TestCase.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User instance | None:
|
||||||
|
If None, no user should be authenticated for this request.
|
||||||
|
Else, the returned user should be authenticated.
|
||||||
|
"""
|
||||||
|
if self.user_auth is not None:
|
||||||
|
return self.user_auth
|
||||||
|
return self.user_auth_mapping.get(action)
|
||||||
|
|
||||||
|
def get_url_model(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the API url for the model.
|
||||||
|
|
||||||
|
Default to a reverse on `{url_label}-list`.
|
||||||
|
|
||||||
|
Used by `[List,Create]APITestCaseMixin`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return reverse(
|
||||||
|
'{label}-list'.format(label=self.url_label),
|
||||||
|
args=args, kwargs=kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url_object(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the API url for a particular object of model.
|
||||||
|
|
||||||
|
Default to a reverse on `{url_label}-detail`.
|
||||||
|
|
||||||
|
Used by `[Retrieve,Update,Destroy]APITestCaseMixin`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return reverse(
|
||||||
|
'{label}-detail'.format(label=self.url_label),
|
||||||
|
args=args, kwargs=kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def db_create(self, data):
|
||||||
|
"""
|
||||||
|
Create a model instance in DB from data given as argument.
|
||||||
|
|
||||||
|
`data` can define M2M fields.
|
||||||
|
|
||||||
|
Operations:
|
||||||
|
- Create object from non-M2M fields.
|
||||||
|
- Link the M2M fields with `set` on the created object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created object.
|
||||||
|
"""
|
||||||
|
fields, m2m_fields = {}, {}
|
||||||
|
for name, value in data.items():
|
||||||
|
if self.model._meta.get_field(name).many_to_many:
|
||||||
|
m2m_fields[name] = value
|
||||||
|
else:
|
||||||
|
fields[name] = value
|
||||||
|
|
||||||
|
instance = self.model.objects.create(**fields)
|
||||||
|
for name, value in m2m_fields.items():
|
||||||
|
getattr(instance, name).set(value)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def get_expected_data(self, instance):
|
||||||
|
"""
|
||||||
|
Returns the data the API should reply for the object `instance`.
|
||||||
|
|
||||||
|
This should be the same result of the serializer used for the sent
|
||||||
|
request.
|
||||||
|
|
||||||
|
Must be defined by subclasses, except for DestroyAPITestCaseMixin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance (a `model` object)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON-decoded data of this instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Subclass must implement 'get_expected_data(instance)' method."
|
||||||
|
)
|
||||||
|
|
||||||
|
def assertInstanceEqual(self, instance, expected):
|
||||||
|
"""
|
||||||
|
Checks if instance verifies expected.
|
||||||
|
|
||||||
|
For each key/value pair of expected, it verifies that instance.key is
|
||||||
|
equal to value. `assertEqual` is used, except for M2M fields where
|
||||||
|
`assertQuerysetEqual(ordered=True, ...)` is used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance (`model` object): E.g. obtained from DB after a create
|
||||||
|
operation.
|
||||||
|
expected (dict): Expected data of instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for key, exp_value in expected.items():
|
||||||
|
field = self.model._meta.get_field(key)
|
||||||
|
value = getattr(instance, key)
|
||||||
|
if field.many_to_many:
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
value.all(), map(repr, exp_value),
|
||||||
|
ordered=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(value, exp_value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instances_data(self):
|
||||||
|
"""
|
||||||
|
Instances data of the model which will be created if necessary.
|
||||||
|
|
||||||
|
For example, ListAPITestCaseMixin uses these before sending the list
|
||||||
|
request.
|
||||||
|
|
||||||
|
The first is also used, if `instance_data` is not re-defined, for RUD
|
||||||
|
operations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Property or attribute 'instances_data' must be declared by "
|
||||||
|
"subclass."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_data(self):
|
||||||
|
"""
|
||||||
|
Data of a single instance of model.
|
||||||
|
|
||||||
|
For example, this data is used to create the instance of model to apply
|
||||||
|
RUD operations.
|
||||||
|
|
||||||
|
Default to the first item of the `instances_data` property.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.instances_data[0]
|
||||||
|
|
||||||
|
|
||||||
|
class ListAPITestCaseMixin:
|
||||||
|
"""
|
||||||
|
Mixin to test the "List" API request of a model.
|
||||||
|
|
||||||
|
Has hooks to check the returned objects and their ordering.
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
This operation use the label 'list'.
|
||||||
|
See `GenericAPITestCaseMixin`#`Authenticate requests` for further
|
||||||
|
informations.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
list_ordering (list of str | str): The ordering of objects that should
|
||||||
|
be respected by the API response.
|
||||||
|
Same format as the ordering Meta attribute of models, e.g.
|
||||||
|
'-created_at' or ['-event', 'created_at'].
|
||||||
|
Ordering on '__' fields are currently not supported.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
* Allow '__' in ordering.
|
||||||
|
* Support pages. Currently we expect returned objects are on a single
|
||||||
|
page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
list_ordering = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_expected(self):
|
||||||
|
"""
|
||||||
|
Model instances the API should returned.
|
||||||
|
|
||||||
|
Default to all objects of models.
|
||||||
|
Eventually sorted according to `list_ordering` attribute.
|
||||||
|
|
||||||
|
"""
|
||||||
|
qs = self.model.objects.all()
|
||||||
|
if self.list_ordering:
|
||||||
|
qs = qs.order_by(self.list_ordering)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_expected_count(self):
|
||||||
|
"""
|
||||||
|
Number of objects the API should returned.
|
||||||
|
|
||||||
|
Default to the length of expected returned objects.
|
||||||
|
"""
|
||||||
|
return len(self.list_expected)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_expected_ordering(self):
|
||||||
|
"""
|
||||||
|
Use this to get expected ordering, instead of relying directly on the
|
||||||
|
`list_ordering`, if you write subclasses.
|
||||||
|
"""
|
||||||
|
if isinstance(self.list_ordering, (tuple, list)):
|
||||||
|
return self.list_ordering
|
||||||
|
return [self.list_ordering]
|
||||||
|
|
||||||
|
def assertOrdered(self, results, ordering):
|
||||||
|
"""
|
||||||
|
Check `results` are sorted according to `ordering`.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
results (list of model instances)
|
||||||
|
ordering (list of fields as str)
|
||||||
|
|
||||||
|
"""
|
||||||
|
_ordering = [
|
||||||
|
(f[1:], True) if f.startswith('-') else (f, False)
|
||||||
|
for f in ordering
|
||||||
|
]
|
||||||
|
|
||||||
|
it = iter(results)
|
||||||
|
|
||||||
|
previous = next(it)
|
||||||
|
|
||||||
|
for current in it:
|
||||||
|
self._assertOrdered(previous, current, _ordering)
|
||||||
|
previous = current
|
||||||
|
|
||||||
|
def _assertOrdered(self, d1, d2, ordering):
|
||||||
|
if not ordering:
|
||||||
|
return True
|
||||||
|
|
||||||
|
field, desc = ordering[0]
|
||||||
|
|
||||||
|
v1, v2 = d1[field], d2[field]
|
||||||
|
|
||||||
|
if v1 == v2:
|
||||||
|
self._assertOrdered(v1, v2, ordering[1:])
|
||||||
|
elif desc:
|
||||||
|
self.assertGreater(v1, v2, msg=(
|
||||||
|
"Ordering on '%s' (DESC) is not respected for the following "
|
||||||
|
"objects.\n- First: %r.\n- Second: %r."
|
||||||
|
% (field, d1, d2)
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.assertLess(v1, v2, msg=(
|
||||||
|
"Ordering on '%s' (ASC) is not respected for the following "
|
||||||
|
"objects:\n- First: %r.\n- Second: %r."
|
||||||
|
% (field, d1, d2)
|
||||||
|
))
|
||||||
|
|
||||||
|
@mock.patch('django.utils.timezone.now')
|
||||||
|
def test_list(self, mock_now):
|
||||||
|
"""
|
||||||
|
The test.
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
- Create instances from `instances_data` property.
|
||||||
|
- Optionally, authenticate client.
|
||||||
|
|
||||||
|
Execution:
|
||||||
|
- HTTP GET request on model url.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- Response status code: 200.
|
||||||
|
- The base response: count (pages not supported).
|
||||||
|
- Results (serialized instances) are formatted as expected and
|
||||||
|
their ordering.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_now.return_value = self.now
|
||||||
|
|
||||||
|
# Setup database.
|
||||||
|
|
||||||
|
for data in self.instances_data:
|
||||||
|
self.db_create(data)
|
||||||
|
|
||||||
|
# Call API to get instances list.
|
||||||
|
user = self.get_user('list')
|
||||||
|
if user:
|
||||||
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
|
r = self.client.get(self.get_url_model())
|
||||||
|
|
||||||
|
# Check API response.
|
||||||
|
self.assertEqual(r.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
base_response = {
|
||||||
|
'count': self.list_expected_count,
|
||||||
|
'previous': None,
|
||||||
|
'next': None,
|
||||||
|
}
|
||||||
|
self.assertDictContainsSubset(base_response, r.data)
|
||||||
|
|
||||||
|
results = r.data['results']
|
||||||
|
|
||||||
|
# Check API response ordering.
|
||||||
|
self.assertOrdered(results, self.list_expected_ordering)
|
||||||
|
|
||||||
|
# Check API response data.
|
||||||
|
for result, instance in zip(results, self.list_expected):
|
||||||
|
self.assertDictEqual(result, self.get_expected_data(instance))
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAPITestCaseMixin:
|
||||||
|
"""
|
||||||
|
Mixin to test the "Create" API request of a model.
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
This operation use the label 'create'.
|
||||||
|
See `GenericAPITestCaseMixin`#`Authenticate requests` for further
|
||||||
|
informations.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The request is sent assuming the payload is JSON.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_data(self):
|
||||||
|
"""
|
||||||
|
Payload of the create request sent to the API.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Subclass must define a 'create_data' attribute/property. This is "
|
||||||
|
"the payload of the POST request to the model url."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_expected(self):
|
||||||
|
"""
|
||||||
|
Data of model instance which should be created.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Subclass must define a create_expected attribute/property. It is "
|
||||||
|
"a dict-like object whose value should be equal to the key "
|
||||||
|
"attribute of the created instance."
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('django.utils.timezone.now')
|
||||||
|
def test_create(self, mock_now):
|
||||||
|
"""
|
||||||
|
The test.
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
- Optionally, authenticate client.
|
||||||
|
|
||||||
|
Execution:
|
||||||
|
- HTTP POST request on model url. Payload from `create_data`.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- Response status code: 201.
|
||||||
|
- Instance has been created in DB.
|
||||||
|
- Instance created is as expected (check with `create_expected`).
|
||||||
|
- Instance is correctly serialized (in response' data).
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_now.return_value = self.now
|
||||||
|
|
||||||
|
# Call API to create an instance of model.
|
||||||
|
user = self.get_user('create')
|
||||||
|
if user:
|
||||||
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
|
r = self.client.post(self.get_url_model(), self.create_data)
|
||||||
|
|
||||||
|
# Check database.
|
||||||
|
instances = self.model.objects.all()
|
||||||
|
self.assertEqual(len(instances), 1)
|
||||||
|
|
||||||
|
instance = instances[0]
|
||||||
|
self.assertInstanceEqual(instance, self.create_expected)
|
||||||
|
|
||||||
|
# Check API response.
|
||||||
|
self.assertEqual(r.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertDictEqual(r.data, self.get_expected_data(instance))
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveAPITestCaseMixin:
|
||||||
|
"""
|
||||||
|
Mixin to test the "Retrieve" API request of a model object.
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
This operation use the label 'retrieve'.
|
||||||
|
See `GenericAPITestCaseMixin`#`Authenticate requests` for further
|
||||||
|
informations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@mock.patch('django.utils.timezone.now')
|
||||||
|
def test_retrieve(self, mock_now):
|
||||||
|
"""
|
||||||
|
The test.
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
- Create a model instance from `instance_data` property.
|
||||||
|
- Optionally, authenticate client.
|
||||||
|
|
||||||
|
Execution:
|
||||||
|
- Get url of the object with its pk.
|
||||||
|
- HTTP GET request on this url.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- Response status code: 200.
|
||||||
|
- Instance is correctly serialized (in response' data).
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_now.return_value = self.now
|
||||||
|
|
||||||
|
# Setup database.
|
||||||
|
data = self.instance_data
|
||||||
|
instance = self.db_create(data)
|
||||||
|
|
||||||
|
# Call API to retrieve the event data.
|
||||||
|
user = self.get_user('retrieve')
|
||||||
|
if user:
|
||||||
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
|
r = self.client.get(self.get_url_object(pk=1))
|
||||||
|
|
||||||
|
# Check API response.
|
||||||
|
self.assertEqual(r.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertDictEqual(r.data, self.get_expected_data(instance))
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAPITestCaseMixin:
|
||||||
|
"""
|
||||||
|
Mixin to test the "Update" API request of a model.
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
This operation use the label 'update'.
|
||||||
|
See `GenericAPITestCaseMixin`#`Authenticate requests` for further
|
||||||
|
informations.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
* A single test using partial update / PATCH HTTP method is written.
|
||||||
|
* The request is sent assuming the payload is JSON.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
* Add test for update / PUT HTTP method.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_data(self):
|
||||||
|
"""
|
||||||
|
Payload of the update request sent to the API.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Subclass must define a update_data attribute/property. This is "
|
||||||
|
"the payload of the PUT request to the instance url."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_expected(self):
|
||||||
|
"""
|
||||||
|
Data of model instance which should be updated.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Subclass must define a update_expected attribute/property. It is "
|
||||||
|
"a dict-like object whose value should be equal to the key "
|
||||||
|
"attribute of the updated instance."
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('django.utils.timezone.now')
|
||||||
|
def test_update(self, mock_now):
|
||||||
|
"""
|
||||||
|
The test.
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
- Create a model instance from `instance_data` property.
|
||||||
|
- Optionally, authenticate client.
|
||||||
|
|
||||||
|
Execution:
|
||||||
|
- Get url of the object with its pk.
|
||||||
|
- HTTP PATCH request on this url. Payload from `update_data`.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- Response status code: 200.
|
||||||
|
- No instance has been created or deleted in DB.
|
||||||
|
- Updated instance is as expected (check with `update_expected`).
|
||||||
|
- Instance is correctly serialized (in response' data).
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_now.return_value = self.now
|
||||||
|
|
||||||
|
# Setup database.
|
||||||
|
data = self.instance_data
|
||||||
|
instance = self.db_create(data)
|
||||||
|
|
||||||
|
# Call API to update the event.
|
||||||
|
user = self.get_user('update')
|
||||||
|
if user:
|
||||||
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
|
r = self.client.patch(self.get_url_object(pk=1), self.update_data)
|
||||||
|
|
||||||
|
# Check database.
|
||||||
|
instances = self.model.objects.all()
|
||||||
|
self.assertEqual(len(instances), 1)
|
||||||
|
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertInstanceEqual(instance, self.update_expected)
|
||||||
|
|
||||||
|
# Check API response.
|
||||||
|
self.assertEqual(r.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertDictEqual(r.data, self.get_expected_data(instance))
|
||||||
|
|
||||||
|
|
||||||
|
class DestroyAPITestCaseMixin:
|
||||||
|
"""
|
||||||
|
Mixin to test the "Destroy" API request of a model.
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
This operation use the label 'destroy'.
|
||||||
|
See `GenericAPITestCaseMixin`#`Authenticate requests` for further
|
||||||
|
informations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@mock.patch('django.utils.timezone.now')
|
||||||
|
def test_destroy(self, mock_now):
|
||||||
|
"""
|
||||||
|
The test.
|
||||||
|
|
||||||
|
Preparation:
|
||||||
|
- Create a model instance from `instance_data` property.
|
||||||
|
- Optionally, authenticate client.
|
||||||
|
|
||||||
|
Execution:
|
||||||
|
- Get url of the object with its pk.
|
||||||
|
- HTTP DELETE request on this url.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- Response status code: 204.
|
||||||
|
- Instance is no longer present in DB.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_now.return_value = self.now
|
||||||
|
|
||||||
|
# Setup database.
|
||||||
|
data = self.instance_data
|
||||||
|
instance = self.db_create(data)
|
||||||
|
|
||||||
|
# Call API to update the event.
|
||||||
|
user = self.get_user('destroy')
|
||||||
|
if user:
|
||||||
|
self.client.force_authenticate(user)
|
||||||
|
|
||||||
|
r = self.client.delete(self.get_url_object(pk=1))
|
||||||
|
|
||||||
|
# Check database.
|
||||||
|
instances = self.model.objects.all()
|
||||||
|
self.assertEqual(len(instances), 0)
|
||||||
|
|
||||||
|
with self.assertRaises(self.model.DoesNotExist):
|
||||||
|
instance.refresh_from_db()
|
||||||
|
|
||||||
|
# Check API response.
|
||||||
|
self.assertEqual(r.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
self.assertIsNone(r.data)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAPITestCaseMixin(
|
||||||
|
ListAPITestCaseMixin, CreateAPITestCaseMixin, RetrieveAPITestCaseMixin,
|
||||||
|
UpdateAPITestCaseMixin, DestroyAPITestCaseMixin,
|
||||||
|
GenericAPITestCaseMixin,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Tests all LCRUD operations on a model.
|
||||||
|
|
||||||
|
See general docs in GenericAPITestCaseMixin.
|
||||||
|
See docs for a specific operation in [Operation]APITestCaseMixin.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
class EventAPITestCase(ModelAPITestCaseMixin, APITestCase):
|
||||||
|
model = Event
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_data(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_expected(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
If you want to run only a few operations, consider using
|
||||||
|
specific-mixins individually. You can still have something as
|
||||||
|
`EventAPITestCaseMixin` to provide common atributes/properties/methods.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
19
api/test/utils.py
Normal file
19
api/test/utils.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def _json_format(value):
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
return value.isoformat().replace('+00:00', 'Z')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def json_format(to_format):
|
||||||
|
"""
|
||||||
|
Returns value formatted like json output of the API.
|
||||||
|
|
||||||
|
Supported type of value:
|
||||||
|
* datetime
|
||||||
|
"""
|
||||||
|
if type(to_format) == dict:
|
||||||
|
return {key: _json_format(value) for key, value in to_format.items()}
|
||||||
|
return _json_format(to_format)
|
292
api/testcases.py
292
api/testcases.py
|
@ -1,292 +0,0 @@
|
||||||
from datetime import timedelta
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from rest_framework.test import APIRequestFactory
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from event.models import Event, Place, ActivityTag, ActivityTemplate
|
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class DataBaseMixin(object):
|
|
||||||
"""
|
|
||||||
provides a datatabse for API tests
|
|
||||||
"""
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
# Users
|
|
||||||
cls.user1_data = {'username': "user1", 'password': "pass1"}
|
|
||||||
cls.user1 = User.objects.create_user(**cls.user1_data)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# Events
|
|
||||||
self.event1_data = {'title': "event1", 'slug': "slug1",
|
|
||||||
'beginning_date': timezone.now()
|
|
||||||
+ timedelta(days=30),
|
|
||||||
"description": "C'est trop cool !",
|
|
||||||
'ending_date': timezone.now()+timedelta(days=31),
|
|
||||||
'created_by': self.user1, }
|
|
||||||
self.event2_data = {"title": "test event", "slug": "test-event",
|
|
||||||
"description": "C'est trop cool !",
|
|
||||||
"beginning_date": "2017-07-18T18:05:00Z",
|
|
||||||
"ending_date": "2017-07-19T18:05:00Z", }
|
|
||||||
self.event1 = Event.objects.create(**self.event1_data)
|
|
||||||
|
|
||||||
# ActivityTags
|
|
||||||
self.tag1_data = {"name": "tag1", "is_public": False, "color": "#111"}
|
|
||||||
self.tag2_data = {"name": "tag2", "is_public": False, "color": "#222"}
|
|
||||||
self.tag3_data = {"name": "tag3", "is_public": False, "color": "#333"}
|
|
||||||
self.tag1 = ActivityTag.objects.create(**self.tag1_data)
|
|
||||||
|
|
||||||
self.act_temp1_data = {'title': "act temp1", 'is_public': True,
|
|
||||||
'remarks': "test remark", 'event': self.event1}
|
|
||||||
self.act_temp2_data = {'title': "act temp2", 'is_public': False,
|
|
||||||
'remarks': "test remark",
|
|
||||||
'tags': [self.tag2_data, ]}
|
|
||||||
self.act_temp1 = ActivityTemplate.objects.create(**self.act_temp1_data)
|
|
||||||
self.act_temp1.tags.add(self.tag1)
|
|
||||||
|
|
||||||
|
|
||||||
class EventBasedModelTestMixin(DataBaseMixin):
|
|
||||||
"""
|
|
||||||
Note : need to define :
|
|
||||||
`model`: the model served by the API
|
|
||||||
`base_name`: the base_name used in the URL
|
|
||||||
`initial_count`: (will disappear) inital count in the db
|
|
||||||
`data_creation`: name in db used to create new instance
|
|
||||||
`instance_name`: existing instance name in the db
|
|
||||||
used for update/delete
|
|
||||||
`field_tested`: name of field tested in the update test
|
|
||||||
`serializer`: serialiser used for the API
|
|
||||||
|
|
||||||
tests for models served by the API that are related to an event
|
|
||||||
and whose API urls are nested under ../event/<event_id>/%model
|
|
||||||
"""
|
|
||||||
def user_create_extra(self):
|
|
||||||
"""
|
|
||||||
extra test in creation by a permited user
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def pre_update_extra(self, data):
|
|
||||||
"""
|
|
||||||
extra modification for the data sent for update
|
|
||||||
"""
|
|
||||||
return data
|
|
||||||
|
|
||||||
def post_update_extra(self):
|
|
||||||
"""
|
|
||||||
extra test for updated model
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_user_create(self):
|
|
||||||
"""
|
|
||||||
ensure a permited user can create a new %model object using API
|
|
||||||
"""
|
|
||||||
data = getattr(self, self.data_creation)
|
|
||||||
|
|
||||||
event_id = self.event1.id
|
|
||||||
url = reverse('{base_name}-list'.format(base_name=self.base_name),
|
|
||||||
kwargs={'event_pk': event_id})
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
response = self.client.post(url, data,
|
|
||||||
format='json')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
||||||
self.assertEqual(self.model.objects.count(), self.initial_count + 1)
|
|
||||||
self.assertEqual(self.model.objects.get(id=self.initial_count+1).event,
|
|
||||||
self.event1)
|
|
||||||
|
|
||||||
self.user_create_extra()
|
|
||||||
|
|
||||||
def test_user_update(self):
|
|
||||||
"""
|
|
||||||
ensure a permited user can update a new %model object using API
|
|
||||||
"""
|
|
||||||
instance = getattr(self, self.instance_name)
|
|
||||||
factory = APIRequestFactory()
|
|
||||||
|
|
||||||
instance_id = instance.id
|
|
||||||
event_id = self.event1.id
|
|
||||||
url = reverse('{base_name}-list'.format(base_name=self.base_name),
|
|
||||||
kwargs={'event_pk': event_id})
|
|
||||||
url = "%s%d/" % (url, instance_id)
|
|
||||||
|
|
||||||
request = factory.get(url)
|
|
||||||
data = self.serializer(instance, context={'request': request}).data
|
|
||||||
|
|
||||||
newvalue = "I'm a test"
|
|
||||||
data[self.field_tested] = newvalue
|
|
||||||
|
|
||||||
data = self.pre_update_extra(data)
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
response = self.client.patch(url, data, format='json')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
instance.refresh_from_db()
|
|
||||||
self.assertEqual(getattr(instance, self.field_tested), newvalue)
|
|
||||||
|
|
||||||
self.post_update_extra(instance)
|
|
||||||
|
|
||||||
def test_user_delete(self):
|
|
||||||
"""
|
|
||||||
ensure a permited user can delete a new %model object using API
|
|
||||||
"""
|
|
||||||
instance = getattr(self, self.instance_name)
|
|
||||||
|
|
||||||
instance_id = instance.id
|
|
||||||
event_id = self.event1.id
|
|
||||||
url = reverse('{base_name}-list'.format(base_name=self.base_name),
|
|
||||||
kwargs={'event_pk': event_id})
|
|
||||||
url = "%s%d/" % (url, instance_id)
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
response = self.client.delete(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
||||||
self.assertEqual(self.model.objects.count(), self.initial_count - 1)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO rajouter la gestion des permissions dans le Mixin
|
|
||||||
# TODO rajouter un test pour s'assurer que les personnes non
|
|
||||||
# connectées ne peuvent pas create/update/delete
|
|
||||||
class EventSpecificTestMixin(object):
|
|
||||||
"""
|
|
||||||
Tests is the EventSpecifics querysets are rendered correctly
|
|
||||||
using the API
|
|
||||||
Note : need to define :
|
|
||||||
`model`: the concerned model serve by the API
|
|
||||||
`root_base_name`: the base_name used in the root-level urls
|
|
||||||
`event_base_name`: the base_name used in the event-level urls
|
|
||||||
|
|
||||||
tests for models served by the API that inherit EventSpecificMixin
|
|
||||||
"""
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
# Users
|
|
||||||
cls.user1_data = {'username': "user1", 'password': "pass1"}
|
|
||||||
cls.user1 = User.objects.create_user(**cls.user1_data)
|
|
||||||
# Events
|
|
||||||
cls.event1_data = {'title': "event1", 'slug': "slug1",
|
|
||||||
'beginning_date': timezone.now()
|
|
||||||
+ timedelta(days=30),
|
|
||||||
'ending_date': timezone.now()+timedelta(days=31),
|
|
||||||
'created_by': cls.user1, }
|
|
||||||
cls.event1 = Event.objects.create(**cls.event1_data)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# Tag
|
|
||||||
self.tag_root_data = {"name": "tag2", "is_public": False,
|
|
||||||
"color": "#222"}
|
|
||||||
self.tag_event_data = {"name": "tag3", "is_public": False,
|
|
||||||
"color": "#333", 'event': self.event1}
|
|
||||||
self.tag_root = ActivityTag.objects.create(**self.tag_root_data)
|
|
||||||
self.tag_event = ActivityTag.objects.create(**self.tag_event_data)
|
|
||||||
|
|
||||||
# Places
|
|
||||||
self.place_root_data = {'name': "place1", 'event': None, }
|
|
||||||
self.place_event_data = {'name': "place2", 'event': self.event1, }
|
|
||||||
self.place_root = Place.objects.create(**self.place_root_data)
|
|
||||||
self.place_event = Place.objects.create(**self.place_event_data)
|
|
||||||
|
|
||||||
def test_lists(self):
|
|
||||||
"""
|
|
||||||
ensure that only root-level models are served under
|
|
||||||
api/%root_base_name/
|
|
||||||
and that root-level and event-level models are served under
|
|
||||||
api/event/<event_id>/%event_base_name/
|
|
||||||
"""
|
|
||||||
event_id = self.event1.id
|
|
||||||
root_count = self.model.objects.filter(event=None).count()
|
|
||||||
event_count = (self.model.objects
|
|
||||||
.filter(Q(event=self.event1) | Q(event=None)).count())
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
|
|
||||||
url = reverse('{base}-list'.format(base=self.root_base_name))
|
|
||||||
response = self.client.get(url, format='json')
|
|
||||||
self.assertEqual(response.json()['count'], root_count)
|
|
||||||
|
|
||||||
event_id = self.event1.id
|
|
||||||
url = reverse('{base}-list'.format(base=self.event_base_name),
|
|
||||||
kwargs={'event_pk': event_id})
|
|
||||||
response = self.client.get(url, format='json')
|
|
||||||
self.assertEqual(response.json()['count'], event_count)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO rajouter la gestion des permissions dans le Mixin
|
|
||||||
# TODO rajouter un test pour s'assurer que les personnes non
|
|
||||||
# connectées ne peuvent pas create/update/delete
|
|
||||||
# TODO? essayer de factoriser avec EventBasedMixin ?
|
|
||||||
# FIXME not working, peut être que le problème vient
|
|
||||||
# du fait que les dates sont mal envoyées dans le data ? A voir.
|
|
||||||
class ModelTestMixin(DataBaseMixin):
|
|
||||||
"""
|
|
||||||
Note : need to define : `model`, `base_name`,
|
|
||||||
`instance_name`, `field_tested`, `serializer`
|
|
||||||
|
|
||||||
generic mixin for testing creation/update/delete
|
|
||||||
of models served by the API
|
|
||||||
"""
|
|
||||||
def test_user_create(self):
|
|
||||||
"""
|
|
||||||
ensure a permited user can create a new %model object using API
|
|
||||||
"""
|
|
||||||
data = getattr(self, self.data_creation)
|
|
||||||
initial_count = self.model.objects.count()
|
|
||||||
|
|
||||||
url = reverse('{base_name}-list'.format(base_name=self.base_name))
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
response = self.client.post(url, data)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
||||||
self.assertEqual(self.model.objects.count(), initial_count + 1)
|
|
||||||
|
|
||||||
instance = self.model.objects.get(id=initial_count+1)
|
|
||||||
for field in self.tested_fields.keys():
|
|
||||||
self.assertEqual(getattr(instance, field), data[field])
|
|
||||||
|
|
||||||
def test_user_update(self):
|
|
||||||
"""
|
|
||||||
ensure a permited user can update a new %model object using API
|
|
||||||
"""
|
|
||||||
instance = getattr(self, self.instance_name)
|
|
||||||
factory = APIRequestFactory()
|
|
||||||
|
|
||||||
instance_id = instance.id
|
|
||||||
url = reverse('{base_name}-detail'.format(base_name=self.base_name),
|
|
||||||
kwargs={'pk': instance_id})
|
|
||||||
|
|
||||||
request = factory.get(url)
|
|
||||||
data = self.serializer(instance, context={'request': request}).data
|
|
||||||
for field, value in self.tested_fields.items():
|
|
||||||
data[field] = value
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
response = self.client.put(url, data, format='json')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
instance.refresh_from_db()
|
|
||||||
for field, value in self.tested_fields.items():
|
|
||||||
self.assertEqual(getattr(instance, field), value)
|
|
||||||
|
|
||||||
def test_user_delete(self):
|
|
||||||
"""
|
|
||||||
ensure a permited user can delete a new %model object using API
|
|
||||||
"""
|
|
||||||
instance = getattr(self, self.instance_name)
|
|
||||||
initial_count = self.model.objects.count()
|
|
||||||
|
|
||||||
instance_id = instance.id
|
|
||||||
url = reverse('{base_name}-detail'.format(base_name=self.base_name),
|
|
||||||
kwargs={'pk': instance_id})
|
|
||||||
|
|
||||||
self.client.force_authenticate(user=self.user1)
|
|
||||||
response = self.client.delete(url)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
||||||
self.assertEqual(self.model.objects.count(), initial_count - 1)
|
|
|
@ -78,6 +78,7 @@ REST_FRAMEWORK = {
|
||||||
'rest_framework.permissions.AllowAny',
|
'rest_framework.permissions.AllowAny',
|
||||||
],
|
],
|
||||||
'PAGE_SIZE': 10,
|
'PAGE_SIZE': 10,
|
||||||
|
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = 'evenementiel.urls'
|
ROOT_URLCONF = 'evenementiel.urls'
|
||||||
|
|
Loading…
Reference in a new issue