diff --git a/pytest.ini b/pytest.ini index 8cc2105..17512bf 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] addopts = --cov - --cov-fail-under 82 + --cov-fail-under 83 --cov-report term:skip-covered --cov-report html --no-cov-on-fail diff --git a/tests/test_views.py b/tests/test_views.py index e450937..712b0eb 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,3 +1,5 @@ +import pytest + from datetime import timedelta from unittest.mock import patch @@ -142,25 +144,12 @@ def test_profile_multipart_create(mock_save, client, db, role, user): @patch('django.core.files.storage.FileSystemStorage.save') -def test_profile_multipart_patch(mock_save, client, db, profile, role, user): - avatar = SimpleUploadedFile("avatar.jpg", b"", content_type="image/jpeg") - mock_save.return_value = "avatar.jpg" - payload = dict(avatar=avatar, role=role.pk, user=user.pk) - response = client.patch(f"/profiles/{profile.pk}/", payload) - result = response.json() - assert response.status_code == 200, result - assert result["avatar"] == "/avatar.jpg" - assert result["role"]["id"] == role.pk - assert result["role"]["name"] == role.name - assert result["user"]["username"] == user.username - - -@patch('django.core.files.storage.FileSystemStorage.save') -def test_profile_multipart_put(mock_save, client, db, profile, role, user): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_multipart_update(mock_save, client, db, method, profile, role, user): avatar = SimpleUploadedFile("avatar.jpg", b"", content_type="image/jpeg") mock_save.return_value = "avatar.jpg" payload = dict(avatar=avatar, role=role.pk, user=user.pk) - response = client.put(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 200, result assert result["avatar"] == "/avatar.jpg" @@ -169,38 +158,43 @@ def test_profile_multipart_put(mock_save, client, db, profile, role, user): assert result["user"]["username"] == user.username -def test_profile_patch_fk(client, db, profile, role, team): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_fk(client, db, method, profile, role, team): payload = dict(role=role.pk, team=team.pk) - response = client.patch(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 200, result assert result["role"]["name"] == role.name assert result["team"]["name"] == team.name -def test_profile_patch_fk_invalid_role(client, db, profile, role, team): - response = client.patch(f"/profiles/{profile.pk}/", dict(role=123)) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_fk_invalid_role(client, db, method, profile, role, team): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(role=123)) result = response.json() assert response.status_code == 422, result assert result["message"] == "Invalid role" -def test_profile_patch_fk_role_is_not_nullable(client, db, profile, role, team): - response = client.patch(f"/profiles/{profile.pk}/", dict(role=None)) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_fk_role_is_not_nullable(client, db, method, profile, role, team): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(role=None)) result = response.json() assert response.status_code == 422, result assert result["message"] == "Invalid role" -def test_profile_patch_fk_team_is_nullable(client, db, profile, role, team): - response = client.patch(f"/profiles/{profile.pk}/", dict(team=None)) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_fk_team_is_nullable(client, db, method, profile, role, team): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(team=None)) result = response.json() assert response.status_code == 200, result assert result["team"] is None -def test_profile_patch_m2m(client, db, profile, tag): - response = client.patch(f"/profiles/{profile.pk}/", dict(tags=[tag.pk])) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m(client, db, method, profile, tag): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(tags=[tag.pk])) result = response.json() assert response.status_code == 200, result assert len(result["tags"]) == 1 @@ -208,31 +202,35 @@ def test_profile_patch_m2m(client, db, profile, tag): assert result["tags"][0]["name"] == tag.name -def test_profile_patch_m2m_can_be_empty(client, db, profile, tag): - response = client.patch(f"/profiles/{profile.pk}/", dict(tags=[])) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_can_be_empty(client, db, method, profile, tag): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(tags=[])) result = response.json() assert response.status_code == 200, result assert len(result["tags"]) == 0 -def test_profile_patch_m2m_is_not_nullable(client, db, profile, tag): - response = client.patch(f"/profiles/{profile.pk}/", dict(tags=None)) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_is_not_nullable(client, db, method, profile, tag): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(tags=None)) result = response.json() assert response.status_code == 422, result assert "tags accepts an array, got None" in result["message"] -def test_profile_patch_m2m_must_be_pks(client, db, profile, tag): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_must_be_pks(client, db, method, profile, tag): payload = dict(tags=["invalid"]) - response = client.patch(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 422, result assert "Invalid tags" in result["message"] -def test_profile_patch_m2m_through(client, db, profile, skill): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_through(client, db, method, profile, skill): payload = dict(skills=[dict(id=skill.pk, rating=4)]) - response = client.patch(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 200, result assert len(result["skills"]) == 1 @@ -241,39 +239,44 @@ def test_profile_patch_m2m_through(client, db, profile, skill): assert result["skills"][0]["rating"] == 4 -def test_profile_patch_m2m_through_can_be_empty(client, db, profile, skill): - response = client.patch(f"/profiles/{profile.pk}/", dict(skills=[])) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_through_can_be_empty(client, db, method, profile, skill): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(skills=[])) result = response.json() assert response.status_code == 200, result assert len(result["skills"]) == 0 -def test_profile_patch_m2m_through_is_not_nullable(client, db, profile, skill): - response = client.patch(f"/profiles/{profile.pk}/", dict(skills=None)) +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_through_is_not_nullable(client, db, method, profile, skill): + response = client.generic(method, f"/profiles/{profile.pk}/", dict(skills=None)) result = response.json() assert response.status_code == 422, result assert "skills accepts an array, got None" in result["message"] -def test_profile_patch_m2m_through_must_be_dicts(client, db, profile, skill): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_through_must_be_dicts(client, db, method, profile, skill): payload = dict(skills=["invalid"]) - response = client.patch(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 422, result assert "Invalid skills" == result["message"] -def test_profile_patch_m2m_through_ids_must_be_pks(client, db, profile, skill): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_through_ids_must_be_pks(client, db, method, profile, skill): payload = dict(skills=[dict(id="invalid")]) - response = client.patch(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 422, result assert "Invalid skills" in result["message"] -def test_profile_patch_m2m_through_required_fields(client, db, profile, skill): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_profile_update_m2m_through_required_fields(client, db, method, profile, skill): payload = dict(skills=[dict(id=skill.pk)]) - response = client.patch(f"/profiles/{profile.pk}/", payload) + response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 422, result assert "Invalid skills" in result["message"] @@ -361,18 +364,38 @@ def test_user_list_multisort(client, now, db, user_factory): assert result["users"][3]["username"] == "a" -def test_user_patch(client, db, user): - payload = dict(username="testtest", email="something@example.com") - response = client.patch(f"/users/{user.pk}/", payload) +def test_user_unique_create_with_existing_value(client, db, user, user_factory): + user_factory.create(username="already_taken") + payload = dict(username="already_taken") + response = client.post("/users/", payload) + result = response.json() + assert response.status_code == 422, result + assert result["message"] == "Field username must be unique" + + +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_user_unique_update_with_existing_value(client, db, method, user, user_factory): + user_factory.create(username="already_taken") + payload = dict(username="already_taken") + response = client.generic(method, f"/users/{user.pk}/", payload) + result = response.json() + assert response.status_code == 422, result + assert result["message"] == "Field username must be unique" + + +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_user_unique_update_with_current_value(client, db, method, user, user_factory): + payload = dict(username=user.username) + response = client.generic(method, f"/users/{user.pk}/", payload) result = response.json() assert response.status_code == 200, result - assert result["username"] == "testtest" - assert result["email"] == "something@example.com" + assert result["username"] == user.username -def test_user_update(client, db, user): +@pytest.mark.parametrize("method", ["PATCH", "PUT"]) +def test_user_update(client, db, method, user): payload = dict(username="testtest", email="something@example.com") - response = client.put(f"/users/{user.pk}/", payload) + response = client.generic(method, f"/users/{user.pk}/", payload) result = response.json() assert response.status_code == 200, result assert result["username"] == "testtest" diff --git a/tests/views.py b/tests/views.py index 34ebe9b..70afb42 100644 --- a/tests/views.py +++ b/tests/views.py @@ -48,11 +48,11 @@ def validate_phone(self, value): try: assert value == "(555) 555-5555" except AssertionError: - raise ValidationError("{value} is not a valid phone number") + raise ValidationError("Field phone is not a valid phone number") return "+5555555555" -class UserList(ListAPI): +class UserList(CreateAPI, ListAPI): model = User ordering = ["pk"] serializer = UserSerializer(only=[ diff --git a/worf/assigns.py b/worf/assigns.py index 808c14c..395a5dc 100644 --- a/worf/assigns.py +++ b/worf/assigns.py @@ -80,6 +80,8 @@ def set_many_to_many_with_through(self, instance, key, value): raise ValidationError(f"Invalid {self.keymap[key]}") from e def validate(self): + instance = self.get_instance() + for key in self.bundle.keys(): self.validate_bundle(key) @@ -87,3 +89,9 @@ def validate(self): if self.bundle[key] is None and not field.null: raise ValidationError(f"Invalid {self.keymap[key]}") + + if field.unique: + other_records = self.model.objects.exclude(pk=instance.pk) + + if other_records.filter(**{key: self.bundle[key]}).exists(): + raise ValidationError(f"Field {self.keymap[key]} must be unique") diff --git a/worf/testing.py b/worf/testing.py index 4d8e989..9ddfccd 100644 --- a/worf/testing.py +++ b/worf/testing.py @@ -7,25 +7,28 @@ class ApiClient(Client): - def generic_multipart(self, method, path, data=None, content_type=None, **kwargs): - content_type = content_type or self._guess_content_type(data) + def generic(self, method, path, data=None, content_type=None, **kwargs): + content_type = content_type or guess_content_type(data) data = self._encode_multipart(data, content_type) - return self.generic(method, path, data, content_type, **kwargs) + return super().generic(method, path, data, content_type, **kwargs) - patch = partialmethod(generic_multipart, "PATCH") - post = partialmethod(generic_multipart, "POST") - put = partialmethod(generic_multipart, "PUT") - - def _contains_files(self, data): - return any(isinstance(value, File) for value in data.values()) + delete = partialmethod(generic, "DELETE") + patch = partialmethod(generic, "PATCH") + post = partialmethod(generic, "POST") + put = partialmethod(generic, "PUT") def _encode_multipart(self, data, content_type): post_data = self._encode_json({} if data is None else data, content_type) return self._encode_data(post_data, content_type) - def _guess_content_type(self, data): - return ( - MULTIPART_CONTENT - if isinstance(data, dict) and self._contains_files(data) - else JSON_CONTENT - ) + +def guess_content_type(data): + return MULTIPART_CONTENT if should_multipart(data) else JSON_CONTENT + + +def has_files(data): + return any(isinstance(value, File) for value in data.values()) + + +def should_multipart(data): + return isinstance(data, dict) and has_files(data)