diff --git a/backend/apps/posts/models.py b/backend/apps/posts/models.py index 41a8801..ed92ceb 100644 --- a/backend/apps/posts/models.py +++ b/backend/apps/posts/models.py @@ -28,7 +28,7 @@ def __str__(self): # !Post class Post(TimeStampedModel): - title = models.CharField(_("Post title"), max_length=250) + title = models.CharField(_("Post title"), max_length=250, unique=True, null=True) author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("Author post"), diff --git a/backend/apps/posts/serializers.py b/backend/apps/posts/serializers.py index 736d644..a7dba1e 100644 --- a/backend/apps/posts/serializers.py +++ b/backend/apps/posts/serializers.py @@ -13,9 +13,11 @@ class Meta: # !CategoryWriteSerializer class CategoryWriteSerializer(serializers.ModelSerializer): + slug = serializers.ReadOnlyField(read_only=True) + class Meta: model = Category - fields = ["name"] + fields = ["name", "slug"] # !PostReadSerializer diff --git a/backend/config/tests/conftest.py b/backend/config/tests/conftest.py index ab90100..336d568 100644 --- a/backend/config/tests/conftest.py +++ b/backend/config/tests/conftest.py @@ -10,6 +10,7 @@ register(factories.PetFactoryEndToEnd) register(factories.TransactionFactory) register(factories.CategoryFactory) +register(factories.PostFactory) # !api_client diff --git a/backend/config/tests/factories.py b/backend/config/tests/factories.py index a0e6ed2..d66b9bb 100644 --- a/backend/config/tests/factories.py +++ b/backend/config/tests/factories.py @@ -9,7 +9,7 @@ from abstract.constants import Genders, GendersPet, Status, Types from apps.pet.models import Pet -from apps.posts.models import Category +from apps.posts.models import Category, Comment, Post from apps.transaction.models import Transaction from apps.user_profile.models import Profile from config.helpers import generate_metamask_address, generate_session_id @@ -148,3 +148,23 @@ class Meta: model = Category name = factory.LazyAttribute(lambda _: faker.word()) + + +# !PostFactory +class PostFactory(factory.django.DjangoModelFactory): + class Meta: + model = Post + + title = factory.LazyAttribute(lambda _: faker.word()) + author = factory.SubFactory(UserFactory) + post_photo_url = factory.LazyAttribute( + lambda _: faker.file_extension(category="image") + ) + body = factory.LazyAttribute(lambda _: faker.paragraph()) + + @factory.post_generation + def categories(self, create, extracted, **kwargs): + if not create: + return + if extracted: + self.categories.add(extracted) diff --git a/backend/config/tests/posts/category/test_post_category.py b/backend/config/tests/posts/category/test_post_category.py index 02fb2f7..aa69e21 100644 --- a/backend/config/tests/posts/category/test_post_category.py +++ b/backend/config/tests/posts/category/test_post_category.py @@ -1,13 +1,7 @@ -import factories import pytest from django.core.exceptions import ValidationError -from django.core.files.uploadedfile import SimpleUploadedFile from django.db import IntegrityError -from abstract.constants import GendersPet, ImageExtension -from apps.pet.models import Pet -from infrastructure.test_data_clear import TruncateTestData - # *If you don't declare pytestmark our test class model don't accsess to database table pytestmark = pytest.mark.django_db @@ -15,24 +9,88 @@ # !TestPostsCategory class TestPostsCategory: def test_str_method(self, category_factory): + """ + Test the __str__ method of the Category model. + + Args: + category_factory: A factory function to create a Category instance. + + Raises: + AssertionError: If the __str__ method does not return the expected value. + + Returns: + None. + """ obj = category_factory() assert obj.__str__() == obj.name def test_slug(self, category_factory): + """ + Test the 'slug' attribute of the Category model. + + Args: + category_factory: A factory function to create a Category instance. + + Raises: + AssertionError: If the 'slug' attribute does not match the expected value. + + Returns: + None. + + """ obj = category_factory(name="category_1") assert obj.slug == obj.name.lower() def test_unique_name(self, category_factory): + """ + Test the uniqueness constraint of the 'name' field in the Category model. + + Args: + category_factory: A factory function to create a Category instance. + + Raises: + IntegrityError: If attempting to create a Category with a non-unique name raises an IntegrityError. + + Returns: + None. + + """ category_factory(name="category_1") with pytest.raises(IntegrityError): category_factory(name="category_1") def test_unique_slug(self, category_factory): + """ + Test the uniqueness constraint of the 'slug' field in the Category model. + + Args: + category_factory: A factory function to create a Category instance. + + Raises: + IntegrityError: If attempting to create a Category with a non-unique slug raises an IntegrityError. + + Returns: + None. + + """ obj = category_factory(name="category_1") with pytest.raises(IntegrityError): category_factory(name=obj.slug) def test_name_max_length(self, category_factory): + """ + Test the maximum length constraint of the 'name' and 'slug' fields in the Category model. + + Args: + category_factory: A factory function to create a Category instance. + + Raises: + ValidationError: If the length of 'name' or 'slug' exceeds the maximum allowed length. + + Returns: + None. + + """ name = "x" * 220 slug = "y" * 240 obj = category_factory(name=name, slug=slug) diff --git a/backend/config/tests/posts/category/test_post_category_endpoint.py b/backend/config/tests/posts/category/test_post_category_endpoint.py index 83e3281..4e03892 100644 --- a/backend/config/tests/posts/category/test_post_category_endpoint.py +++ b/backend/config/tests/posts/category/test_post_category_endpoint.py @@ -16,6 +16,12 @@ class TestPostsCategoryEndpoints: endpoint = "/posts/categories/" def get_register_payload(self): + """ + Returns a dictionary representing the payload for user registration. + + Returns: + dict: The payload containing user information. + """ return { "username": "User", "email": "user@mail.ru", @@ -23,41 +29,154 @@ def get_register_payload(self): "wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", } - @pytest.fixture - def category_response(self, api_client): - payload = {"name": "devops"} - response = api_client().post(f"{self.endpoint}", payload, format="json") - yield payload, response + def get_staff_user(self, user_id): + """ + Retrieves a user by their ID and sets them as a staff user. + + Parameters: + user_id (int): The ID of the user. + + Returns: + User: The modified user object. + """ + user = User.objects.get(id=user_id) + user.is_staff = True + user.save() + return user @pytest.fixture def register_response(self, api_client): + """ + Performs a user registration request using the provided API client. + + Parameters: + api_client (function): A callable representing the API client. + + Yields: + tuple: A tuple containing the payload and the registration response. + """ payload = self.get_register_payload() response = api_client().post("/users/register/", payload, format="json") yield payload, response @pytest.fixture def get_user(self, register_response, api_client): + """ + Retrieves user information using the provided registration response and API client. + + Parameters: + register_response (tuple): A tuple containing the payload and registration response. + api_client (function): A callable representing the API client. + + Yields: + tuple: A tuple containing the user and request headers. + """ payload, response = register_response access_token = response.data["tokens"]["access"] headers = {"Authorization": f"Bearer {access_token}"} user = api_client().get("/users/", headers=headers) yield user, headers + @pytest.fixture + def category_response(self, api_client, get_user): + """ + Performs a category creation request using the provided API client and user information. + + Parameters: + api_client (function): A callable representing the API client. + get_user (tuple): A tuple containing the user information and request headers. + + Yields: + tuple: A tuple containing the payload and the category creation response. + """ + user, headers = get_user + user = self.get_staff_user(user.data["id"]) + if user.is_staff and user.is_authenticated: + payload = {"name": "devops", "slug": "devops"} + response = api_client().post( + f"{self.endpoint}", payload, format="json", headers=headers + ) + yield payload, response + def test_return_all_categories(self, category_factory, api_client): + """ + Test case for returning all categories. + + Parameters: + category_factory: A factory object for creating categories. + api_client (function): A callable representing the API client. + """ category_factory.create_batch(4) response = api_client().get(self.endpoint, format="json") assert response.status_code == status.HTTP_200_OK assert len(json.loads(response.content)) == 4 - def test_create_category(self, get_user, api_client): + def test_create_category(self, category_response, api_client): + """ + Test case for creating a category. + + Parameters: + category_response (tuple): A tuple containing the payload and category creation response. + api_client (function): A callable representing the API client. + """ + payload, response = category_response + assert response.status_code == status.HTTP_201_CREATED + assert json.loads(response.content) == payload + + def test_get_category(self, category_response, api_client): + """ + Test case for retrieving a category. + + Parameters: + category_response (tuple): A tuple containing the payload and category creation response. + api_client (function): A callable representing the API client. + """ + payload, response = category_response + category = api_client().get( + f"{self.endpoint}{response.data['slug']}/", format="json" + ) + assert category.status_code == status.HTTP_200_OK + + def test_update_category(self, category_response, get_user, api_client): + """ + Test case for updating a category. + + Parameters: + category_response (tuple): A tuple containing the payload and category creation response. + get_user (tuple): A tuple containing the user information and request headers. + api_client (function): A callable representing the API client. + """ + payload, response = category_response user, headers = get_user - user = User.objects.get(id=user.data["id"]) - user.is_staff = True - user.save() - if user.is_superuser and user.is_authenticated: - payload = {"name": "devops"} - response = api_client().post( - f"{self.endpoint}", payload, format="json", headers=headers - ) - assert response.status_code == status.HTTP_201_CREATED - assert json.loads(response.content) == payload + updated_category_obj = {"name": "backend"} + updated_category = api_client().put( # patch and put same thing for this process because we have only update one field for each request + f"{self.endpoint}{response.data['slug']}/", + updated_category_obj, + headers=headers, + format="json", + ) + assert updated_category.status_code == status.HTTP_200_OK + assert json.loads(updated_category.content) != updated_category_obj + assert ( + json.loads(updated_category.content)["name"] == updated_category_obj["name"] + ) + assert ( + json.loads(updated_category.content)["slug"] == updated_category_obj["name"] + ) + assert json.loads(updated_category.content)["slug"] != response.data["slug"] + + def test_delete_category(self, category_response, get_user, api_client): + """ + Test case for deleting a category. + + Parameters: + category_response (tuple): A tuple containing the payload and category creation response. + get_user (tuple): A tuple containing the user information and request headers. + api_client (function): A callable representing the API client. + """ + payload, response = category_response + user, headers = get_user + category = api_client().delete( + f"{self.endpoint}{response.data['slug']}/", headers=headers, format="json" + ) + assert category.status_code == status.HTTP_204_NO_CONTENT diff --git a/backend/config/tests/posts/post/test_post.py b/backend/config/tests/posts/post/test_post.py new file mode 100644 index 0000000..067e915 --- /dev/null +++ b/backend/config/tests/posts/post/test_post.py @@ -0,0 +1,157 @@ +import json + +import pytest +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from rest_framework import status + +from apps.posts.models import Post + +# * If you don't declare pytestmark our test class model don't accsess to database table +pytestmark = pytest.mark.django_db + +# User +User = get_user_model() + + +# !TestPosts +class TestPosts: + def test_post_factory_creation(self, post_factory, category_factory): + """ + Test the creation of a Post instance using the post_factory. + + Args: + post_factory: A factory function to create a Post instance. + category_factory: A factory function to create a Category instance. + + Raises: + AssertionError: If any of the assertions fail, indicating a test failure. + + Returns: + None. + + """ + obj = post_factory(categories=category_factory().id) + assert isinstance(obj, Post) + assert obj.title is not None + assert obj.author is not None + assert obj.post_photo_url is not None + assert obj.body is not None + assert len(obj.categories.all()) > 0 + + def test_str_method(self, post_factory, category_factory): + """ + Test the __str__ method of the Post model. + + Args: + post_factory: A factory function to create a Post instance. + category_factory: A factory function to create a Category instance. + + Raises: + AssertionError: If the __str__ method does not return the expected value. + + Returns: + None. + + """ + obj = post_factory(categories=category_factory().id) + assert obj.__str__() == f"{obj.title} by {obj.author.username}" + + def test_slug(self, post_factory): + """ + Test the 'slug' attribute of the Post model. + + Args: + post_factory: A factory function to create a Post instance. + + Raises: + AssertionError: If the 'slug' attribute does not match the expected value. + + Returns: + None. + + """ + obj = post_factory(title="System design why is the so important") + assert obj.slug.replace("-", " ") == obj.title.lower() + + def test_unique_title(self, post_factory): + """ + Test the uniqueness constraint of the 'title' field in the Post model. + + Args: + post_factory: A factory function to create a Post instance. + + Raises: + IntegrityError: If attempting to create a Post with a non-unique title raises an IntegrityError. + + Returns: + None. + + """ + post_factory(title="Python web framework") + with pytest.raises(IntegrityError): + post_factory(title="Python web framework") + + def test_unique_slug(self, post_factory): + """ + Test the uniqueness constraint of the 'slug' field in the Post model. + + Args: + post_factory: A factory function to create a Post instance. + + Raises: + IntegrityError: If attempting to create a Post with a non-unique slug raises an IntegrityError. + + Returns: + None. + + """ + obj = post_factory(title="Javascript vs Elixer") + with pytest.raises(IntegrityError): + post_factory(title=obj.slug) + + def test_name_max_length(self, post_factory): + """ + Test the maximum length constraint of the 'title' and 'slug' fields in the Post model. + + Args: + post_factory: A factory function to create a Post instance. + + Raises: + ValidationError: If the length of 'title' or 'slug' exceeds the maximum allowed length. + + Returns: + None. + + """ + title = "x" * 280 + slug = "y" * 150 + obj = post_factory(title=title, slug=slug) + if len(title) > 250 or len(slug) > 100: + with pytest.raises(ValidationError): + obj.full_clean() + else: + assert len(title) <= 250 + assert len(slug) <= 100 + + def test_post_factory_with_categories(self, post_factory, category_factory): + """ + Test the creation of a Post instance with associated categories using the post_factory. + + Args: + post_factory: A factory function to create a Post instance. + category_factory: A factory function to create a Category instance. + + Raises: + AssertionError: If any of the assertions fail, indicating a test failure. + + Returns: + None. + + """ + category = category_factory().id + post = post_factory(categories=category) + assert isinstance(post, Post) + assert post.categories.count() == 1 + assert Post.objects.filter(categories__id=category).exists()