From 37aebba747d5a1eab24c6591291636e48c3724b8 Mon Sep 17 00:00:00 2001 From: Celia Date: Sun, 16 Nov 2025 17:25:51 +0100 Subject: [PATCH] Tests de funcionalidad de stock --- essenza/product/tests.py | 170 ++++++++++++++++++++++++++++++++++++--- essenza/product/views.py | 105 +++++++++++++----------- 2 files changed, 218 insertions(+), 57 deletions(-) diff --git a/essenza/product/tests.py b/essenza/product/tests.py index 2f03894..6c96642 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1,14 +1,16 @@ +from decimal import Decimal + +from django.contrib.auth import get_user_model +from django.contrib.messages import get_messages # Para probar mensajes from django.test import TestCase from django.urls import reverse -from django.contrib.auth import get_user_model -from product.models import Product, Category -from django.core.files.uploadedfile import SimpleUploadedFile -from decimal import Decimal + +from product.models import Category, Product + User = get_user_model() class ProductCRUDTests(TestCase): - def setUp(self): self.user = User.objects.create_user( username="user", @@ -29,10 +31,10 @@ def setUp(self): description="Descripción", brand="Marca X", price=10, - photo= None, + photo=None, stock=5, category="maquillaje", - is_active=True + is_active=True, ) # URLs self.list_url = reverse("product_list") @@ -66,7 +68,7 @@ def test_create_requires_login(self): def test_user_cannot_access_list(self): self.client.login(username="user", password="pass1234") resp = self.client.get(self.list_url) - self.assertEqual(resp.status_code, 302) # Redirigido por permisos + self.assertEqual(resp.status_code, 302) # Redirigido por permisos def test_user_cannot_access_detail(self): self.client.login(username="user", password="pass1234") @@ -84,22 +86,21 @@ def test_user_cannot_access_create(self): def test_admin_can_access_list(self): self.client.force_login(self.admin) - url = reverse('product_list') + url = reverse("product_list") resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertContains(resp, "Producto Test") - def test_admin_can_access_detail(self): self.client.force_login(self.admin) - url = reverse('product_detail', args=[self.product.pk]) + url = reverse("product_detail", args=[self.product.pk]) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertContains(resp, self.product.name) def test_admin_can_delete_product(self): self.client.force_login(self.admin) - url = reverse('product_delete', args=[self.product.pk]) + url = reverse("product_delete", args=[self.product.pk]) # GET renderiza el confirm delete resp_get = self.client.get(url) @@ -212,3 +213,148 @@ def test_catalog_detail_returns_404_for_nonexistent_product(self): url = reverse("catalog_detail", args=[9999]) response = self.client.get(url) self.assertEqual(response.status_code, 404) + + +class StockTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + username="user", + email="user@example.com", + password="pass1234", + role="user", + ) + cls.admin = User.objects.create_user( + username="admin", + email="admin@example.com", + password="pass1234", + role="admin", + ) + + cls.product_high = Product.objects.create( + name="Producto Alto", stock=20, price=10 + ) + cls.product_low = Product.objects.create( + name="Producto Bajo", stock=5, price=10 + ) + cls.product_out = Product.objects.create( + name="Producto Agotado", stock=0, price=10 + ) + + cls.stock_url = reverse("stock") + cls.login_url = reverse("login") + cls.dashboard_url = reverse("dashboard") + + # --- TESTS DE ACCESO --- + + def test_anonymous_user_redirects_to_dashboard(self): + resp = self.client.get(self.stock_url) + self.assertEqual(resp.status_code, 302) + self.assertRedirects(resp, self.dashboard_url) + + def test_non_admin_user_redirects_to_dashboard(self): + self.client.login(email=self.user.email, password="pass1234") + resp = self.client.get(self.stock_url) + self.assertEqual(resp.status_code, 302) + self.assertRedirects(resp, self.dashboard_url) + + def test_admin_user_succeeds_get(self): + self.client.login(email=self.admin.email, password="pass1234") + resp = self.client.get(self.stock_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/stock.html") + + # --- TESTS DE FUNCIONALIDAD --- + + def test_stock_page_shows_all_products(self): + self.client.login(email=self.admin.email, password="pass1234") + resp = self.client.get(self.stock_url) + + # Comprobamos que aparecen los 3 productos + self.assertEqual(len(resp.context["products"]), 3) + + # Comprobamos el HTML + self.assertContains(resp, "Producto Alto") + self.assertContains( + resp, 'En Stock: 20', html=True + ) + + self.assertContains(resp, "Producto Bajo") + self.assertContains( + resp, 'Stock Bajo: 5', html=True + ) + + self.assertContains(resp, "Producto Agotado") + self.assertContains( + resp, 'Agotado (0)', html=True + ) + + def test_post_admin_updates_stock_successfully(self): + """5. CORRECTO: Un admin puede actualizar el stock (Test 7 en tu código).""" + self.client.login(email=self.admin.email, password="pass1234") + + self.assertEqual(self.product_high.stock, 20) # Stock inicial + + data = {"product_id": self.product_high.pk, "stock": 15} + resp = self.client.post( + self.stock_url, data, follow=True + ) # Se hace una petición POST para actualizar el stock a 15 + + # Comprobamos que volvemos a la página de stock + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/stock.html") + + # Comprobamos que la base de datos se actualizó correctamente + self.product_high.refresh_from_db() + self.assertEqual(self.product_high.stock, 15) + + # Comprobamos el mensaje de éxito + messages = list(get_messages(resp.context["request"])) + self.assertEqual(len(messages), 1) + self.assertEqual(str(messages[0]), "Stock de 'Producto Alto' actualizado a 15.") + + def test_post_admin_invalid_product_returns_404(self): + self.client.login(email=self.admin.email, password="pass1234") + data = {"product_id": 999, "stock": 15} # ID 999 no existe + + resp = self.client.post(self.stock_url, data) + self.assertEqual(resp.status_code, 404) + + def test_post_admin_invalid_stock_value_shows_error(self): + self.client.login(email=self.admin.email, password="pass1234") + + # Enviamos un valor de stock no numérico + data = {"product_id": self.product_high.pk, "stock": "abc"} + resp = self.client.post(self.stock_url, data, follow=True) + + # Comprobamos que volvemos a la página de stock + self.assertEqual(resp.status_code, 200) + + # Comprobamos que el stock NO se actualizó + self.product_high.refresh_from_db() + self.assertEqual(self.product_high.stock, 20) + + # Comprobamos el mensaje de error + messages = list(get_messages(resp.context["request"])) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), "El valor de stock 'abc' no es un número válido." + ) + + def test_post_admin_negative_stock_value_shows_error(self): + self.client.login(email=self.admin.email, password="pass1234") + + # Enviamos un valor de stock negativo y comprobamos el error + data = {"product_id": self.product_high.pk, "stock": "-5"} + resp = self.client.post(self.stock_url, data, follow=True) + + self.assertEqual(resp.status_code, 200) + + self.product_high.refresh_from_db() + self.assertEqual(self.product_high.stock, 20) # No cambia + + messages = list(get_messages(resp.context["request"])) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), "El valor de stock '-5' no es un número válido." + ) diff --git a/essenza/product/views.py b/essenza/product/views.py index 0550e2a..9b0e177 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -3,10 +3,11 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views import View -from .forms import ProductForm from order.models import OrderProduct + +from .forms import ProductForm from .models import Product -from django.shortcuts import render, get_object_or_404 + class DashboardView(View): template_name = "product/dashboard.html" @@ -41,7 +42,10 @@ class StockView(LoginRequiredMixin, UserPassesTestMixin, View): def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" - # Solo se ejecutan métodos GET y POST si el usuario pasa la prueba + # Redirige a 'dashboard' si no pasa el test_func + def handle_no_permission(self): + return redirect("dashboard") + def get(self, request): # Carga y muestra todos los productos ordenados por nombre products = Product.objects.all().order_by("name") @@ -50,98 +54,109 @@ def get(self, request): def post(self, request): # Coge datos del formulario para actualizar stock product_id = request.POST.get("product_id") - stock = request.POST.get("stock") - - # Encuentra el producto por su ID - product = Product.objects.get(pk=product_id) - if not product: - messages.error(request, "Producto no encontrado.") - return redirect("stock") - - # Actualiza el stock del producto - new_stock = int(stock or 0) - product.stock = new_stock - product.save(update_fields=["stock"]) - messages.success( - request, f"Stock de '{product.name}' actualizado a {new_stock}." - ) + stock_value = request.POST.get("stock") # Renombrado para claridad + + product = get_object_or_404(Product, pk=product_id) + + try: + # Comprobamos si el valor es un número + new_stock = int(stock_value or 0) + if new_stock < 0: + # No permitir stock negativo + raise ValueError("El stock no puede ser negativo") + + product.stock = new_stock + product.save(update_fields=["stock"]) + messages.success( + request, f"Stock de '{product.name}' actualizado a {new_stock}." + ) + + except (ValueError, TypeError): + messages.error( + request, f"El valor de stock '{stock_value}' no es un número válido." + ) # Recarga la misma página return redirect("stock") - -class ProductListView(LoginRequiredMixin, UserPassesTestMixin,View): - template_name = 'product/list.html' + +class ProductListView(LoginRequiredMixin, UserPassesTestMixin, View): + template_name = "product/list.html" def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" def get(self, request): products = Product.objects.all() - return render(request, self.template_name, {'products': products}) + return render(request, self.template_name, {"products": products}) + -class ProductDetailView(LoginRequiredMixin, UserPassesTestMixin,View): - template_name = 'product/detail.html' +class ProductDetailView(LoginRequiredMixin, UserPassesTestMixin, View): + template_name = "product/detail.html" def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" def get(self, request, pk): product = get_object_or_404(Product, pk=pk) - return render(request, self.template_name, {'product': product}) - -class ProductCreateView(LoginRequiredMixin, UserPassesTestMixin,View): - template_name = 'product/form.html' + return render(request, self.template_name, {"product": product}) + + +class ProductCreateView(LoginRequiredMixin, UserPassesTestMixin, View): + template_name = "product/form.html" form_class = ProductForm + def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" def get(self, request): form = self.form_class() - return render(request, self.template_name, {'form': form}) + return render(request, self.template_name, {"form": form}) def post(self, request): form = self.form_class(request.POST, request.FILES) if form.is_valid(): form.save() - return redirect('product_list') - return render(request, self.template_name, {'form': form}) + return redirect("product_list") + return render(request, self.template_name, {"form": form}) -class ProductUpdateView(LoginRequiredMixin, UserPassesTestMixin,View): - template_name = 'product/form.html' + +class ProductUpdateView(LoginRequiredMixin, UserPassesTestMixin, View): + template_name = "product/form.html" form_class = ProductForm - + def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" - + def get(self, request, pk): product = get_object_or_404(Product, pk=pk) form = self.form_class(instance=product) - return render(request, self.template_name, {'form': form, 'product': product}) - + return render(request, self.template_name, {"form": form, "product": product}) def post(self, request, pk): product = get_object_or_404(Product, pk=pk) form = self.form_class(request.POST, request.FILES, instance=product) if form.is_valid(): form.save() - return redirect('product_detail', pk=product.pk) - return render(request, self.template_name, {'form': form, 'product': product}) + return redirect("product_detail", pk=product.pk) + return render(request, self.template_name, {"form": form, "product": product}) + + +class ProductDeleteView(LoginRequiredMixin, UserPassesTestMixin, View): + template_name = "product/confirm_delete.html" -class ProductDeleteView(LoginRequiredMixin, UserPassesTestMixin,View): - template_name = 'product/confirm_delete.html' def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" - + def get(self, request, pk): product = get_object_or_404(Product, pk=pk) - return render(request, self.template_name, {'product': product}) - + return render(request, self.template_name, {"product": product}) def post(self, request, pk): product = get_object_or_404(Product, pk=pk) product.delete() - return redirect('product_list') + return redirect("product_list") + class CatalogView(View): template_name = "product/catalog.html"