Skip to content

Commit

Permalink
Unique validation
Browse files Browse the repository at this point in the history
  • Loading branch information
stevelacey committed Feb 3, 2022
1 parent da00ded commit 45face0
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 69 deletions.
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down
125 changes: 74 additions & 51 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from datetime import timedelta
from unittest.mock import patch

Expand Down Expand Up @@ -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"
Expand All @@ -169,70 +158,79 @@ 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
assert result["tags"][0]["id"] == tag.pk
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 <class 'NoneType'> 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
Expand All @@ -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 <class 'NoneType'> 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"]
Expand Down Expand Up @@ -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="[email protected]")
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"] == "[email protected]"
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="[email protected]")
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"
Expand Down
4 changes: 2 additions & 2 deletions tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
8 changes: 8 additions & 0 deletions worf/assigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,18 @@ 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)

field = self.model._meta.get_field(key)

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")
33 changes: 18 additions & 15 deletions worf/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 45face0

Please sign in to comment.