feat: add catalogue image management (#1)
- Add CatalogueImage model with FK to Product - Add image resize service (strict/non-strict modes with configurable dimensions) - Add CRUD API endpoints with admin-only write permissions - Add catalogue_images field to product listing/detail endpoints - Serve media files in development via static() - 28 TDD tests covering model, API, permissions, resize, and product listing
This commit is contained in:
415
tienda_ilusion/don_confiao/tests/test_catalogue_images.py
Normal file
415
tienda_ilusion/don_confiao/tests/test_catalogue_images.py
Normal file
@@ -0,0 +1,415 @@
|
||||
import io
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import override_settings
|
||||
from PIL import Image
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from ..models.products import Product
|
||||
from .Mixins import LoginMixin
|
||||
|
||||
|
||||
def _create_test_image(width=600, height=400, name="test.png"):
|
||||
"""Create a SimpleUploadedFile from an in-memory image using PIL."""
|
||||
img = Image.new("RGBA", (width, height), (255, 0, 0, 255))
|
||||
output = io.BytesIO()
|
||||
img.save(output, format="PNG")
|
||||
return SimpleUploadedFile(
|
||||
name=name,
|
||||
content=output.getvalue(),
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
|
||||
class TestCatalogueImageModel(APITestCase, LoginMixin):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
self.product = Product.objects.create(
|
||||
name="Test Product", price=100.00
|
||||
)
|
||||
|
||||
def test_create_catalogue_image(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
image_file = _create_test_image()
|
||||
catalogue_image = CatalogueImage.objects.create(
|
||||
product=self.product, image=image_file
|
||||
)
|
||||
self.assertIsInstance(catalogue_image, CatalogueImage)
|
||||
self.assertEqual(catalogue_image.product, self.product)
|
||||
self.assertIsNotNone(catalogue_image.image)
|
||||
self.assertIsNotNone(catalogue_image.uploaded_at)
|
||||
|
||||
def test_catalogue_image_product_relation(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
image_file = _create_test_image()
|
||||
ci = CatalogueImage.objects.create(
|
||||
product=self.product, image=image_file
|
||||
)
|
||||
self.assertIn(ci, self.product.catalogue_images.all())
|
||||
|
||||
def test_catalogue_image_str(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
image_file = _create_test_image()
|
||||
ci = CatalogueImage.objects.create(
|
||||
product=self.product, image=image_file
|
||||
)
|
||||
expected = f"CatalogueImage for {self.product.name} ({ci.id})"
|
||||
self.assertEqual(str(ci), expected)
|
||||
|
||||
def test_cascade_delete_with_product(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
image_file = _create_test_image()
|
||||
CatalogueImage.objects.create(
|
||||
product=self.product, image=image_file
|
||||
)
|
||||
self.assertEqual(CatalogueImage.objects.count(), 1)
|
||||
self.product.delete()
|
||||
self.assertEqual(CatalogueImage.objects.count(), 0)
|
||||
|
||||
|
||||
class TestCatalogueImageAPIPermissions(APITestCase, LoginMixin):
|
||||
def setUp(self):
|
||||
self.product = Product.objects.create(
|
||||
name="Perm Test Product", price=100.00
|
||||
)
|
||||
|
||||
def test_create_catalogue_image_unauthenticated(self):
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
image_file = _create_test_image()
|
||||
data = {"product": self.product.id, "image": image_file}
|
||||
response = self.client.post(url, data, format="multipart")
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_create_catalogue_image_non_admin(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="regularuser",
|
||||
email="regular@example.com",
|
||||
password="regularpass",
|
||||
)
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework.test import APIClient
|
||||
refresh = RefreshToken.for_user(self.user)
|
||||
self.client = APIClient()
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||
)
|
||||
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
image_file = _create_test_image()
|
||||
data = {"product": self.product.id, "image": image_file}
|
||||
response = self.client.post(url, data, format="multipart")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_get_catalogue_images_authenticated_non_admin(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="regularuser2",
|
||||
email="regular2@example.com",
|
||||
password="regularpass",
|
||||
)
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework.test import APIClient
|
||||
refresh = RefreshToken.for_user(self.user)
|
||||
self.client = APIClient()
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||
)
|
||||
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_delete_catalogue_image_non_admin(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
admin_user = User.objects.create_superuser(
|
||||
username="admin2", email="admin2@example.com", password="adminpass"
|
||||
)
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework.test import APIClient
|
||||
admin_refresh = RefreshToken.for_user(admin_user)
|
||||
admin_client = APIClient()
|
||||
admin_client.credentials(
|
||||
HTTP_AUTHORIZATION=f"Bearer {str(admin_refresh.access_token)}"
|
||||
)
|
||||
|
||||
image_file = _create_test_image()
|
||||
create_data = {
|
||||
"product": self.product.id, "image": image_file
|
||||
}
|
||||
create_response = admin_client.post(
|
||||
"/don_confiao/api/catalogue_images/", create_data, format="multipart"
|
||||
)
|
||||
ci_id = create_response.json()["id"]
|
||||
|
||||
regular_user = User.objects.create_user(
|
||||
username="regularuser3",
|
||||
email="regular3@example.com",
|
||||
password="regularpass",
|
||||
)
|
||||
refresh = RefreshToken.for_user(regular_user)
|
||||
self.client = APIClient()
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||
)
|
||||
|
||||
response = self.client.delete(
|
||||
f"/don_confiao/api/catalogue_images/{ci_id}/"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
self.assertEqual(CatalogueImage.objects.count(), 1)
|
||||
|
||||
|
||||
class TestCatalogueImageAPI(APITestCase, LoginMixin):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
self.product = Product.objects.create(
|
||||
name="API Test Product", price=100.00
|
||||
)
|
||||
self.product2 = Product.objects.create(
|
||||
name="API Test Product 2", price=200.00
|
||||
)
|
||||
|
||||
def _create_image(self, product=None, width=600, height=400):
|
||||
if product is None:
|
||||
product = self.product
|
||||
image_file = _create_test_image(width=width, height=height)
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
data = {"product": product.id, "image": image_file}
|
||||
return self.client.post(url, data, format="multipart")
|
||||
|
||||
def test_list_catalogue_images_empty(self):
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.json(), [])
|
||||
|
||||
def test_create_catalogue_image(self):
|
||||
response = self._create_image()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
data = response.json()
|
||||
self.assertIn("id", data)
|
||||
self.assertEqual(data["product"], self.product.id)
|
||||
self.assertIn("image", data)
|
||||
self.assertIn("uploaded_at", data)
|
||||
|
||||
def test_list_catalogue_images(self):
|
||||
self._create_image()
|
||||
self._create_image(product=self.product2)
|
||||
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.json()), 2)
|
||||
|
||||
def test_retrieve_catalogue_image(self):
|
||||
create_response = self._create_image()
|
||||
ci_id = create_response.json()["id"]
|
||||
|
||||
url = f"/don_confiao/api/catalogue_images/{ci_id}/"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.json()["id"], ci_id)
|
||||
self.assertEqual(response.json()["product"], self.product.id)
|
||||
|
||||
def test_update_catalogue_image(self):
|
||||
create_response = self._create_image()
|
||||
ci_id = create_response.json()["id"]
|
||||
|
||||
new_image = _create_test_image(width=200, height=200, name="updated.png")
|
||||
url = f"/don_confiao/api/catalogue_images/{ci_id}/"
|
||||
response = self.client.put(
|
||||
url,
|
||||
{"product": self.product.id, "image": new_image},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_delete_catalogue_image(self):
|
||||
create_response = self._create_image()
|
||||
ci_id = create_response.json()["id"]
|
||||
|
||||
url = f"/don_confiao/api/catalogue_images/{ci_id}/"
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
self.assertEqual(CatalogueImage.objects.count(), 0)
|
||||
|
||||
def test_create_catalogue_image_invalid_product(self):
|
||||
image_file = _create_test_image()
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
data = {"product": 99999, "image": image_file}
|
||||
response = self.client.post(url, data, format="multipart")
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_create_catalogue_image_missing_file(self):
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
data = {"product": self.product.id}
|
||||
response = self.client.post(url, data, format="multipart")
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestCatalogueImageResize(APITestCase, LoginMixin):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
self.product = Product.objects.create(
|
||||
name="Resize Test Product", price=100.00
|
||||
)
|
||||
|
||||
def _create_and_get_image_obj(self, width, height, strict=False,
|
||||
bg_color=None):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
settings_overrides = {
|
||||
"CATALOGUE_IMAGE_WIDTH": 600,
|
||||
"CATALOGUE_IMAGE_HEIGHT": 600,
|
||||
"CATALOGUE_STRICT_DIMENSION": strict,
|
||||
}
|
||||
if bg_color is not None:
|
||||
settings_overrides["CATALOGUE_BACKGROUND_IMAGES_COLOR"] = bg_color
|
||||
|
||||
with override_settings(**settings_overrides):
|
||||
image_file = _create_test_image(width=width, height=height)
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
data = {"product": self.product.id, "image": image_file}
|
||||
response = self.client.post(url, data, format="multipart")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
ci_id = response.json()["id"]
|
||||
return CatalogueImage.objects.get(id=ci_id)
|
||||
|
||||
def test_resize_non_strict_mode_landscape(self):
|
||||
ci = self._create_and_get_image_obj(
|
||||
width=800, height=600, strict=False
|
||||
)
|
||||
with Image.open(ci.image.path) as img:
|
||||
self.assertEqual(img.width, 600)
|
||||
expected_height = int(600 * 600 / 800)
|
||||
self.assertEqual(img.height, expected_height)
|
||||
|
||||
def test_resize_non_strict_mode_portrait(self):
|
||||
ci = self._create_and_get_image_obj(
|
||||
width=600, height=800, strict=False
|
||||
)
|
||||
with Image.open(ci.image.path) as img:
|
||||
self.assertEqual(img.width, 600)
|
||||
expected_height = int(800 * 600 / 600)
|
||||
self.assertEqual(img.height, expected_height)
|
||||
|
||||
def test_resize_non_strict_mode_square(self):
|
||||
ci = self._create_and_get_image_obj(
|
||||
width=600, height=600, strict=False
|
||||
)
|
||||
with Image.open(ci.image.path) as img:
|
||||
self.assertEqual(img.width, 600)
|
||||
self.assertEqual(img.height, 600)
|
||||
|
||||
def test_resize_strict_mode_landscape(self):
|
||||
ci = self._create_and_get_image_obj(
|
||||
width=800, height=600, strict=True
|
||||
)
|
||||
with Image.open(ci.image.path) as img:
|
||||
self.assertEqual(img.width, 600)
|
||||
self.assertEqual(img.height, 600)
|
||||
|
||||
def test_resize_strict_mode_portrait(self):
|
||||
ci = self._create_and_get_image_obj(
|
||||
width=600, height=800, strict=True
|
||||
)
|
||||
with Image.open(ci.image.path) as img:
|
||||
self.assertEqual(img.width, 600)
|
||||
self.assertEqual(img.height, 600)
|
||||
|
||||
def test_resize_strict_mode_with_color_background(self):
|
||||
ci = self._create_and_get_image_obj(
|
||||
width=800, height=600, strict=True, bg_color="#FFFFFF"
|
||||
)
|
||||
with Image.open(ci.image.path) as img:
|
||||
self.assertEqual(img.width, 600)
|
||||
self.assertEqual(img.height, 600)
|
||||
|
||||
@override_settings(CATALOGUE_MAX_UPLOAD_SIZE=1)
|
||||
def test_resize_large_file_rejected(self):
|
||||
image_file = _create_test_image(width=10, height=10)
|
||||
url = "/don_confiao/api/catalogue_images/"
|
||||
data = {"product": self.product.id, "image": image_file}
|
||||
response = self.client.post(url, data, format="multipart")
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class TestProductListingWithImages(APITestCase, LoginMixin):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
self.product = Product.objects.create(
|
||||
name="Product With Images", price=100.00
|
||||
)
|
||||
self.product_no_images = Product.objects.create(
|
||||
name="Product Without Images", price=200.00
|
||||
)
|
||||
|
||||
def test_product_list_includes_catalogue_images_field(self):
|
||||
url = "/don_confiao/api/products/"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for product in response.json():
|
||||
self.assertIn("catalogue_images", product)
|
||||
|
||||
def test_product_list_catalogue_images_urls(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
image_file = _create_test_image()
|
||||
CatalogueImage.objects.create(
|
||||
product=self.product, image=image_file
|
||||
)
|
||||
|
||||
url = "/don_confiao/api/products/"
|
||||
response = self.client.get(url)
|
||||
data = response.json()
|
||||
|
||||
product_data = next(
|
||||
p for p in data if p["id"] == self.product.id
|
||||
)
|
||||
self.assertEqual(len(product_data["catalogue_images"]), 1)
|
||||
self.assertTrue(
|
||||
product_data["catalogue_images"][0].endswith(".png")
|
||||
)
|
||||
|
||||
def test_product_list_catalogue_images_multiple(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
img1 = _create_test_image(name="img1.png")
|
||||
img2 = _create_test_image(name="img2.png")
|
||||
CatalogueImage.objects.create(product=self.product, image=img1)
|
||||
CatalogueImage.objects.create(product=self.product, image=img2)
|
||||
|
||||
url = "/don_confiao/api/products/"
|
||||
response = self.client.get(url)
|
||||
data = response.json()
|
||||
|
||||
product_data = next(
|
||||
p for p in data if p["id"] == self.product.id
|
||||
)
|
||||
self.assertEqual(len(product_data["catalogue_images"]), 2)
|
||||
|
||||
def test_product_list_catalogue_images_empty(self):
|
||||
url = "/don_confiao/api/products/"
|
||||
response = self.client.get(url)
|
||||
data = response.json()
|
||||
|
||||
product_data = next(
|
||||
p for p in data if p["id"] == self.product_no_images.id
|
||||
)
|
||||
self.assertEqual(product_data["catalogue_images"], [])
|
||||
|
||||
def test_product_detail_includes_catalogue_images(self):
|
||||
from ..models.catalogue_images import CatalogueImage
|
||||
image_file = _create_test_image()
|
||||
CatalogueImage.objects.create(
|
||||
product=self.product, image=image_file
|
||||
)
|
||||
|
||||
url = f"/don_confiao/api/products/{self.product.id}/"
|
||||
response = self.client.get(url)
|
||||
data = response.json()
|
||||
self.assertIn("catalogue_images", data)
|
||||
self.assertEqual(len(data["catalogue_images"]), 1)
|
||||
Reference in New Issue
Block a user