diff --git a/tienda/migrations/__pycache__/0001_initial.cpython-312.pyc b/tienda/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index b63c144..0000000 Binary files a/tienda/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-312.pyc b/tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-312.pyc deleted file mode 100644 index c83e2a8..0000000 Binary files a/tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-312.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0003_order_transaction_code.cpython-312.pyc b/tienda/migrations/__pycache__/0003_order_transaction_code.cpython-312.pyc deleted file mode 100644 index a1ecd9e..0000000 Binary files a/tienda/migrations/__pycache__/0003_order_transaction_code.cpython-312.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0004_product_stock_stockreservation_stockreservationitem.cpython-312.pyc b/tienda/migrations/__pycache__/0004_product_stock_stockreservation_stockreservationitem.cpython-312.pyc deleted file mode 100644 index 93a7921..0000000 Binary files a/tienda/migrations/__pycache__/0004_product_stock_stockreservation_stockreservationitem.cpython-312.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0005_savedpaymentmethod.cpython-312.pyc b/tienda/migrations/__pycache__/0005_savedpaymentmethod.cpython-312.pyc deleted file mode 100644 index 3fc5dc1..0000000 Binary files a/tienda/migrations/__pycache__/0005_savedpaymentmethod.cpython-312.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/__init__.cpython-312.pyc b/tienda/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 7168695..0000000 Binary files a/tienda/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/tienda/models.py b/tienda/models.py index 52004b3..56d986e 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models from django.contrib.auth.models import User, AbstractUser from django.utils.crypto import get_random_string @@ -314,4 +316,4 @@ class ShippingAddress(models.Model): # Si se marca como predeterminada, desmarcar las demás del usuario if self.is_default: ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False) - super().save(*args, **kwargs) \ No newline at end of file + super().save(*args, **kwargs) diff --git a/tienda/templates/tienda/aviso_legal.html b/tienda/templates/tienda/aviso_legal.html index a3fc895..a847648 100644 --- a/tienda/templates/tienda/aviso_legal.html +++ b/tienda/templates/tienda/aviso_legal.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
diff --git a/tienda/templates/tienda/ayuda.html b/tienda/templates/tienda/ayuda.html index 97582ac..0d336c5 100644 --- a/tienda/templates/tienda/ayuda.html +++ b/tienda/templates/tienda/ayuda.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
diff --git a/tienda/templates/tienda/cookies.html b/tienda/templates/tienda/cookies.html index 17fa473..3fdee13 100644 --- a/tienda/templates/tienda/cookies.html +++ b/tienda/templates/tienda/cookies.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
diff --git a/tienda/templates/tienda/devoluciones.html b/tienda/templates/tienda/devoluciones.html index c64ffbd..613c82e 100644 --- a/tienda/templates/tienda/devoluciones.html +++ b/tienda/templates/tienda/devoluciones.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
diff --git a/tienda/templates/tienda/rgpd.html b/tienda/templates/tienda/rgpd.html index 09546c1..37ff7bb 100644 --- a/tienda/templates/tienda/rgpd.html +++ b/tienda/templates/tienda/rgpd.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
@@ -61,4 +61,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tienda/templates/tienda/sobre_nosotros.html b/tienda/templates/tienda/sobre_nosotros.html index 8120358..5ce9573 100644 --- a/tienda/templates/tienda/sobre_nosotros.html +++ b/tienda/templates/tienda/sobre_nosotros.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
diff --git a/tienda/templates/tienda/terminos.html b/tienda/templates/tienda/terminos.html index 5a47506..31a36d6 100644 --- a/tienda/templates/tienda/terminos.html +++ b/tienda/templates/tienda/terminos.html @@ -1,5 +1,5 @@ -{% load static %} {% extends "tienda/base.html" %} +{% load static %} {% block content %}
diff --git a/tienda/templatetags/__pycache__/__init__.cpython-312.pyc b/tienda/templatetags/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 2dd2093..0000000 Binary files a/tienda/templatetags/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/tienda/templatetags/__pycache__/vat_filters.cpython-312.pyc b/tienda/templatetags/__pycache__/vat_filters.cpython-312.pyc deleted file mode 100644 index f30aca6..0000000 Binary files a/tienda/templatetags/__pycache__/vat_filters.cpython-312.pyc and /dev/null differ diff --git a/tienda/tests.py b/tienda/tests.py index 640d51a..1f6b083 100644 --- a/tienda/tests.py +++ b/tienda/tests.py @@ -1,7 +1,11 @@ -from django.test import TestCase +import json +from unittest.mock import MagicMock, patch + +from django.test import TestCase, override_settings from django.contrib.auth import get_user_model from django.utils import timezone from django.db import IntegrityError +from django.urls import reverse from datetime import timedelta from .models import ( User, VerificationCode, Category, Image, Product, @@ -1251,4 +1255,612 @@ class ShippingAddressModelTests(TestCase): address_str = str(address) self.assertIn("John Doe", address_str) - self.assertIn("Almería", address_str) \ No newline at end of file + self.assertIn("Almería", address_str) + + +@override_settings( + CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}, + SESSION_ENGINE="django.contrib.sessions.backends.db", +) +class EndpointViewTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.password = "StrongPassword123!" + cls.buyer = User.objects.create_user( + username="buyer", + email="buyer@example.com", + password=cls.password, + registration_status=User.RegisterStatus.ACTIVE, + ) + cls.seller = User.objects.create_user( + username="seller", + email="seller@example.com", + password=cls.password, + registration_status=User.RegisterStatus.ACTIVE, + ) + cls.other_user = User.objects.create_user( + username="other", + email="other@example.com", + password=cls.password, + registration_status=User.RegisterStatus.ACTIVE, + ) + cls.category = Category.objects.create(name="Electrónica") + cls.image = Image.objects.create(name="imagen", image="images/test.jpg") + cls.product = Product.objects.create( + name="Producto test", + briefdesc="Breve", + description="Descripción", + price=10.0, + stock=20, + category=cls.category, + primary_image=cls.image, + creator=cls.seller, + ) + cls.address = ShippingAddress.objects.create( + user=cls.buyer, + full_name="Comprador Uno", + address_line_1="Calle Mayor 1", + city="Almería", + postal_code="04001", + phone="600000001", + is_default=True, + ) + cls.other_address = ShippingAddress.objects.create( + user=cls.other_user, + full_name="Otro Usuario", + address_line_1="Calle Otro 2", + city="Almería", + postal_code="04002", + phone="600000002", + ) + + def _login(self, user=None): + self.client.force_login(user or self.buyer) + + def _create_cart_item(self, quantity=1, user=None): + owner = user or self.buyer + cart, _ = Cart.objects.get_or_create(user=owner) + item, _ = CartItem.objects.get_or_create(cart=cart, product=self.product, defaults={"quantity": quantity}) + item.quantity = quantity + item.save() + return item + + def _post_json(self, url_name, data, **kwargs): + return self.client.post( + reverse(url_name, kwargs=kwargs or None), + data=json.dumps(data), + content_type="application/json", + ) + + def test_public_endpoints_render(self): + public_routes = [ + reverse("home"), + reverse("index"), + reverse("productos"), + reverse("producto", args=[self.product.id]), + reverse("categoria", args=[self.category.id]), + reverse("search"), + reverse("search_suggestions"), + reverse("login"), + reverse("register"), + reverse("rgpd"), + reverse("privacidad"), + reverse("devoluciones"), + reverse("aviso_legal"), + reverse("terminos"), + reverse("cookies"), + reverse("sobre_nosotros"), + reverse("ayuda"), + reverse("reset_password"), + ] + for url in public_routes: + with self.subTest(url=url): + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_login_required_endpoints_redirect_anonymous(self): + secured_get_routes = [ + reverse("mis_productos"), + reverse("pedidos_vendedor"), + reverse("crear_producto"), + reverse("checkout"), + reverse("checkout_success"), + reverse("checkout_cancel"), + reverse("portal_usuario"), + reverse("mis_compras"), + reverse("mis_recibos"), + reverse("editar_perfil"), + reverse("direcciones_usuario"), + reverse("crear_direccion"), + reverse("mensajes_comprador"), + reverse("metodos_pago"), + reverse("agregar_tarjeta"), + reverse("agregar_paypal"), + reverse("editar_producto", args=[self.product.id]), + reverse("borrar_producto", args=[self.product.id]), + reverse("cambiar_estado_pedido", args=[1]), + reverse("enviar_mensaje_pedido", args=[1]), + reverse("editar_direccion", args=[self.address.id]), + reverse("eliminar_direccion", args=[self.address.id]), + reverse("eliminar_metodo_pago", args=[1]), + ] + for url in secured_get_routes: + with self.subTest(url=url): + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + self.assertIn(reverse("login"), response.url) + + secured_post_routes = [ + "crear_payment_intent", + "confirmar_pago_tarjeta", + "crear_orden_paypal", + "capturar_orden_paypal", + "crear_setup_intent", + "confirmar_setup_intent", + "crear_orden_paypal_setup", + "capturar_orden_paypal_setup", + ] + for name in secured_post_routes: + with self.subTest(name=name): + response = self.client.post(reverse(name)) + self.assertEqual(response.status_code, 302) + self.assertIn(reverse("login"), response.url) + + @patch("tienda.views.tasks.enviar_correo_confirmacion.delay") + @patch("tienda.views.tasks.enviar_correo_bienvenida.delay") + def test_register_login_logout_and_verify_flows(self, welcome_delay, confirm_delay): + register_response = self.client.post(reverse("register"), data={ + "name": "Nuevo", + "email": "nuevo@example.com", + "password": self.password, + "password_confirm": self.password, + }) + self.assertEqual(register_response.status_code, 302) + confirm_delay.assert_called_once() + + created_user = User.objects.get(email="nuevo@example.com") + created_user.registration_status = User.RegisterStatus.ACTIVE + created_user.save(update_fields=["registration_status"]) + + login_response = self.client.post(reverse("login"), data={ + "email": "nuevo@example.com", + "password": self.password, + "remember": "on", + }) + self.assertEqual(login_response.status_code, 302) + self.assertEqual(login_response.url, reverse("index")) + welcome_delay.assert_called_once() + + logout_response = self.client.get(reverse("logout")) + self.assertEqual(logout_response.status_code, 302) + self.assertEqual(logout_response.url, reverse("index")) + + verification = VerificationCode.generate(created_user, VerificationCode.VerificationModes.VERIFY_ACCOUNT) + verify_response = self.client.get(reverse("verify", args=[verification.code])) + self.assertEqual(verify_response.status_code, 302) + created_user.refresh_from_db() + self.assertEqual(created_user.registration_status, User.RegisterStatus.ACTIVE) + + invalid_verify_response = self.client.get(reverse("verify", args=["codigo-invalido"])) + self.assertEqual(invalid_verify_response.status_code, 200) + + @patch("tienda.views.tasks.enviar_correo_recuperacion.delay") + def test_password_reset_endpoints(self, recovery_delay): + reset_get = self.client.get(reverse("reset_password")) + self.assertEqual(reset_get.status_code, 200) + reset_post = self.client.post(reverse("reset_password"), data={"email": self.buyer.email}) + self.assertEqual(reset_post.status_code, 200) + recovery_delay.assert_called_once_with(self.buyer.email) + + code = VerificationCode.generate(self.buyer, VerificationCode.VerificationModes.RESET_PASSWORD) + phase2_get = self.client.get(reverse("reset_password_phase2", args=[code.code])) + self.assertEqual(phase2_get.status_code, 200) + + mismatch = self.client.post(reverse("reset_password_phase2", args=[code.code]), data={ + "password": "NuevaPassword123!", + "verify_password": "DistintaPassword123!", + }) + self.assertEqual(mismatch.status_code, 200) + + success = self.client.post(reverse("reset_password_phase2", args=[code.code]), data={ + "password": "NuevaPassword123!", + "verify_password": "NuevaPassword123!", + }) + self.assertEqual(success.status_code, 302) + self.assertTrue(User.objects.get(id=self.buyer.id).check_password("NuevaPassword123!")) + + not_found = self.client.get(reverse("reset_password_phase2", args=["no-existe"])) + self.assertEqual(not_found.status_code, 404) + + def test_search_and_suggestions(self): + response = self.client.get(reverse("search"), data={"q": "Producto"}) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Producto test") + + suggestions = self.client.get(reverse("search_suggestions"), data={"q": "Pr"}) + self.assertEqual(suggestions.status_code, 200) + payload = suggestions.json() + self.assertTrue(payload["suggestions"]) + + def test_cart_endpoints(self): + self._login() + cart_view = self.client.get(reverse("view_cart")) + self.assertEqual(cart_view.status_code, 200) + + add_response = self.client.post(reverse("add_to_cart", args=[self.product.id]), data={"quantity": 2}) + self.assertEqual(add_response.status_code, 302) + item = CartItem.objects.get(cart__user=self.buyer, product=self.product) + self.assertEqual(item.quantity, 2) + + update_response = self.client.post(reverse("update_cart_item", args=[item.id]), data={"quantity": 3}) + self.assertEqual(update_response.status_code, 302) + item.refresh_from_db() + self.assertEqual(item.quantity, 3) + + remove_response = self.client.post(reverse("remove_from_cart", args=[item.id])) + self.assertEqual(remove_response.status_code, 302) + self.assertFalse(CartItem.objects.filter(id=item.id).exists()) + + self._create_cart_item(quantity=1) + clear_response = self.client.post(reverse("clear_cart")) + self.assertEqual(clear_response.status_code, 302) + self.assertEqual(CartItem.objects.filter(cart__user=self.buyer).count(), 0) + + def test_seller_panel_endpoints(self): + self._login(self.seller) + order = Order.objects.create( + buyer=self.buyer, + shipping_address=self.address, + total=12.1, + payment_method=Order.PAYMENT_STRIPE, + ) + item = OrderItem.objects.create( + order=order, + product=self.product, + product_name=self.product.name, + seller=self.seller, + quantity=1, + unit_price=12.1, + total_price=12.1, + ) + + self.assertEqual(self.client.get(reverse("mis_productos")).status_code, 200) + self.assertEqual(self.client.get(reverse("pedidos_vendedor")).status_code, 200) + self.assertEqual(self.client.get(reverse("crear_producto")).status_code, 200) + self.assertEqual(self.client.get(reverse("editar_producto", args=[self.product.id])).status_code, 200) + + create_response = self.client.post(reverse("crear_producto"), data={ + "name": "Nuevo producto", + "briefdesc": "Breve", + "description": "Descripción", + "price": "25.50", + "stock": "5", + "category": str(self.category.id), + }) + self.assertEqual(create_response.status_code, 302) + created = Product.objects.get(name="Nuevo producto") + + edit_response = self.client.post(reverse("editar_producto", args=[created.id]), data={ + "name": "Producto editado", + "briefdesc": "Actualizado", + "description": "Descripción nueva", + "price": "30.00", + "stock": "6", + "category": str(self.category.id), + }) + self.assertEqual(edit_response.status_code, 302) + created.refresh_from_db() + self.assertEqual(created.name, "Producto editado") + + status_response = self.client.post(reverse("cambiar_estado_pedido", args=[item.id]), data={ + "estado": OrderItem.STATUS_PROCESSING, + }) + self.assertEqual(status_response.status_code, 302) + item.refresh_from_db() + self.assertEqual(item.status, OrderItem.STATUS_PROCESSING) + + message_response = self.client.post(reverse("enviar_mensaje_pedido", args=[item.id]), data={"mensaje": "Preparando envío"}) + self.assertEqual(message_response.status_code, 302) + self.assertTrue(OrderMessage.objects.filter(order_item=item, sender=self.seller).exists()) + + delete_get = self.client.get(reverse("borrar_producto", args=[created.id])) + self.assertEqual(delete_get.status_code, 302) + delete_post = self.client.post(reverse("borrar_producto", args=[created.id])) + self.assertEqual(delete_post.status_code, 302) + self.assertFalse(Product.objects.filter(id=created.id).exists()) + + @patch("tienda.views.stripe.PaymentIntent.create") + @patch("tienda.views._create_stock_reservation_for_cart") + def test_crear_payment_intent_endpoint(self, reservation_mock, create_pi_mock): + self._login() + self._create_cart_item(quantity=2) + reservation_mock.return_value = (MagicMock(id=321), []) + create_pi_mock.return_value = MagicMock(client_secret="secret", id="pi_123") + + self.assertEqual(self.client.get(reverse("crear_payment_intent")).status_code, 405) + bad_json = self.client.post(reverse("crear_payment_intent"), data="{", content_type="application/json") + self.assertEqual(bad_json.status_code, 400) + + missing_address = self._post_json("crear_payment_intent", {}) + self.assertEqual(missing_address.status_code, 400) + + ok = self._post_json("crear_payment_intent", {"shipping_address_id": self.address.id, "save_card": True}) + self.assertEqual(ok.status_code, 200) + data = ok.json() + self.assertEqual(data["client_secret"], "secret") + self.assertEqual(data["payment_intent_id"], "pi_123") + + @patch("tienda.views.create_order_from_cart") + @patch("tienda.views.stripe.PaymentIntent.retrieve") + def test_confirmar_pago_tarjeta_endpoint(self, retrieve_mock, create_order_mock): + self._login() + self._create_cart_item(quantity=1) + session = self.client.session + session["selected_shipping_address_id"] = self.address.id + session.save() + + retrieve_mock.return_value = MagicMock(status="succeeded") + order = Order.objects.create( + buyer=self.buyer, + shipping_address=self.address, + total=12.1, + payment_method=Order.PAYMENT_STRIPE, + ) + create_order_mock.return_value = (order, "") + + self.assertEqual(self.client.get(reverse("confirmar_pago_tarjeta")).status_code, 405) + self.assertEqual(self.client.post(reverse("confirmar_pago_tarjeta"), data="{", content_type="application/json").status_code, 400) + self.assertEqual(self._post_json("confirmar_pago_tarjeta", {}).status_code, 400) + + ok = self._post_json("confirmar_pago_tarjeta", {"payment_intent_id": "pi_ok"}) + self.assertEqual(ok.status_code, 200) + self.assertTrue(ok.json()["success"]) + + @patch("tienda.views._paypal_create_order") + @patch("tienda.views._create_stock_reservation_for_cart") + def test_crear_orden_paypal_endpoint(self, reservation_mock, create_order_mock): + self._login() + self._create_cart_item(quantity=1) + reservation_mock.return_value = (MagicMock(id=555), []) + create_order_mock.return_value = {"id": "ORDER123"} + + self.assertEqual(self.client.get(reverse("crear_orden_paypal")).status_code, 405) + missing_address = self._post_json("crear_orden_paypal", {}) + self.assertEqual(missing_address.status_code, 400) + + ok = self._post_json("crear_orden_paypal", {"shipping_address_id": self.address.id}) + self.assertEqual(ok.status_code, 200) + self.assertEqual(ok.json()["id"], "ORDER123") + + @patch("tienda.views.create_order_from_cart") + @patch("tienda.views._paypal_capture_order") + def test_capturar_orden_paypal_endpoint(self, capture_mock, create_order_mock): + self._login() + self._create_cart_item(quantity=1) + session = self.client.session + session["paypal_order_id"] = "ORDER123" + session["selected_shipping_address_id"] = self.address.id + session.save() + + order = Order.objects.create( + buyer=self.buyer, + shipping_address=self.address, + total=12.1, + payment_method=Order.PAYMENT_PAYPAL, + ) + create_order_mock.return_value = (order, "") + + self.assertEqual(self.client.get(reverse("capturar_orden_paypal")).status_code, 405) + self.assertEqual(self.client.post(reverse("capturar_orden_paypal"), data="{", content_type="application/json").status_code, 400) + self.assertEqual(self._post_json("capturar_orden_paypal", {}).status_code, 400) + self.assertEqual(self._post_json("capturar_orden_paypal", {"orderID": "WRONG"}).status_code, 400) + + capture_mock.return_value = {"status": "APPROVED"} + not_completed = self._post_json("capturar_orden_paypal", {"orderID": "ORDER123"}) + self.assertEqual(not_completed.status_code, 400) + + capture_mock.return_value = { + "status": "COMPLETED", + "payer": {"email_address": "paypal@example.com", "payer_id": "payer_123"}, + } + ok = self._post_json("capturar_orden_paypal", {"orderID": "ORDER123", "save_paypal": True}) + self.assertEqual(ok.status_code, 200) + self.assertTrue(ok.json()["success"]) + self.assertTrue(SavedPaymentMethod.objects.filter(user=self.buyer, paypal_email="paypal@example.com").exists()) + + @patch("tienda.views.create_order_from_cart") + @patch("paypalrestsdk.Payment.find") + @patch("paypalrestsdk.configure") + def test_paypal_legacy_endpoints(self, configure_mock, find_mock, create_order_mock): + self._login() + self._create_cart_item(quantity=1) + + self.assertEqual(self.client.get(reverse("create_paypal_payment")).status_code, 405) + + with patch("paypalrestsdk.Payment") as payment_cls, patch("tienda.views._create_stock_reservation_for_cart") as reservation_mock: + reservation_mock.return_value = (MagicMock(id=777), []) + payment_instance = MagicMock() + payment_instance.create.return_value = True + payment_instance.id = "PAY-123" + payment_instance.links = [MagicMock(rel="approval_url", href="https://paypal.local/approve")] + payment_cls.return_value = payment_instance + create_payment = self.client.post(reverse("create_paypal_payment"), data={"shipping_address_id": self.address.id}) + self.assertEqual(create_payment.status_code, 200) + self.assertIn("redirect", create_payment.json()) + + missing_data = self.client.get(reverse("paypal_execute")) + self.assertEqual(missing_data.status_code, 302) + + session = self.client.session + session["paypal_payment_id"] = "PAY-123" + session["selected_shipping_address_id"] = self.address.id + session.save() + payment_found = MagicMock() + payment_found.execute.return_value = True + find_mock.return_value = payment_found + order = Order.objects.create( + buyer=self.buyer, + shipping_address=self.address, + total=12.1, + payment_method=Order.PAYMENT_PAYPAL, + ) + create_order_mock.return_value = (order, "") + execute = self.client.get(reverse("paypal_execute"), data={"PayerID": "payer"}) + self.assertEqual(execute.status_code, 200) + + @patch("tienda.views.stripe.SetupIntent.create") + @patch("tienda.views._get_or_create_stripe_customer") + def test_setup_intent_endpoints(self, customer_mock, setup_mock): + self._login() + self.assertEqual(self.client.get(reverse("metodos_pago")).status_code, 200) + self.assertEqual(self.client.get(reverse("agregar_tarjeta")).status_code, 200) + self.assertEqual(self.client.get(reverse("agregar_paypal")).status_code, 200) + + self.assertEqual(self.client.get(reverse("crear_setup_intent")).status_code, 405) + customer_mock.return_value = "cus_123" + setup_mock.return_value = MagicMock(client_secret="seti_secret") + setup_response = self.client.post(reverse("crear_setup_intent")) + self.assertEqual(setup_response.status_code, 200) + self.assertEqual(setup_response.json()["customer_id"], "cus_123") + + self.assertEqual(self.client.get(reverse("confirmar_setup_intent")).status_code, 405) + self.assertEqual(self.client.post(reverse("confirmar_setup_intent"), data="{", content_type="application/json").status_code, 400) + self.assertEqual(self._post_json("confirmar_setup_intent", {}).status_code, 400) + + with patch("tienda.views.stripe.PaymentMethod.attach"), patch("tienda.views.stripe.PaymentMethod.retrieve") as retrieve_pm: + retrieve_pm.return_value = MagicMock( + card=MagicMock(brand="visa", last4="4242", exp_month=1, exp_year=2030) + ) + confirm = self._post_json("confirmar_setup_intent", {"payment_method_id": "pm_123"}) + self.assertEqual(confirm.status_code, 200) + self.assertTrue(confirm.json()["success"]) + + @patch("tienda.views._paypal_create_order") + def test_paypal_setup_endpoints(self, create_order_mock): + self._login() + self.assertEqual(self.client.get(reverse("crear_orden_paypal_setup")).status_code, 405) + create_order_mock.return_value = {"id": "ORDER_SETUP"} + create_response = self.client.post(reverse("crear_orden_paypal_setup")) + self.assertEqual(create_response.status_code, 200) + self.assertEqual(create_response.json()["id"], "ORDER_SETUP") + + self.assertEqual(self.client.get(reverse("capturar_orden_paypal_setup")).status_code, 405) + self.assertEqual(self.client.post(reverse("capturar_orden_paypal_setup"), data="{", content_type="application/json").status_code, 400) + self.assertEqual(self._post_json("capturar_orden_paypal_setup", {}).status_code, 400) + + with patch("tienda.views._paypal_capture_order") as capture_mock: + capture_mock.return_value = {"status": "COMPLETED", "payer": {"email_address": "payer@example.com", "payer_id": "payer_1"}} + capture_response = self._post_json("capturar_orden_paypal_setup", {"orderID": "ORDER_SETUP"}) + self.assertEqual(capture_response.status_code, 200) + self.assertTrue(capture_response.json()["success"]) + + def test_user_portal_and_addresses_endpoints(self): + self._login() + order = Order.objects.create( + buyer=self.buyer, + shipping_address=self.address, + total=12.1, + payment_method=Order.PAYMENT_STRIPE, + ) + item = OrderItem.objects.create( + order=order, + product=self.product, + product_name=self.product.name, + seller=self.seller, + quantity=1, + unit_price=12.1, + total_price=12.1, + ) + OrderMessage.objects.create(order_item=item, sender=self.seller, message="Mensaje vendedor") + + self.assertEqual(self.client.get(reverse("portal_usuario")).status_code, 200) + self.assertEqual(self.client.get(reverse("mis_compras")).status_code, 200) + self.assertEqual(self.client.get(reverse("mis_recibos")).status_code, 200) + self.assertEqual(self.client.get(reverse("mensajes_comprador")).status_code, 200) + self.assertEqual(self.client.get(reverse("direcciones_usuario")).status_code, 200) + self.assertEqual(self.client.get(reverse("crear_direccion")).status_code, 200) + self.assertEqual(self.client.get(reverse("editar_direccion", args=[self.address.id])).status_code, 200) + + profile = self.client.post(reverse("editar_perfil"), data={ + "first_name": "Nombre", + "last_name": "Apellido", + "email": "buyer-updated@example.com", + }) + self.assertEqual(profile.status_code, 302) + self.buyer.refresh_from_db() + self.assertEqual(self.buyer.email, "buyer-updated@example.com") + + wrong_current = self.client.post(reverse("cambiar_contrasena"), data={ + "current_password": "incorrecta", + "new_password": "PasswordNueva123!", + "confirm_password": "PasswordNueva123!", + }) + self.assertEqual(wrong_current.status_code, 200) + changed = self.client.post(reverse("cambiar_contrasena"), data={ + "current_password": self.password, + "new_password": "PasswordNueva123!", + "confirm_password": "PasswordNueva123!", + }) + self.assertEqual(changed.status_code, 302) + self.buyer.refresh_from_db() + self.assertTrue(self.buyer.check_password("PasswordNueva123!")) + + invalid_city = self.client.post(reverse("crear_direccion"), data={ + "full_name": "Comprador Uno", + "address_line_1": "Calle Nueva 3", + "city": "Madrid", + "postal_code": "04003", + "phone": "600000003", + }) + self.assertEqual(invalid_city.status_code, 200) + invalid_postal = self.client.post(reverse("crear_direccion"), data={ + "full_name": "Comprador Uno", + "address_line_1": "Calle Nueva 3", + "city": "Almería", + "postal_code": "28001", + "phone": "600000003", + }) + self.assertEqual(invalid_postal.status_code, 200) + + create_ok = self.client.post(reverse("crear_direccion"), data={ + "full_name": "Comprador Dos", + "address_line_1": "Calle Nueva 3", + "city": "Almería", + "postal_code": "04003", + "phone": "600000003", + "is_default": "on", + }) + self.assertEqual(create_ok.status_code, 302) + new_address = ShippingAddress.objects.get(full_name="Comprador Dos") + self.assertEqual(new_address.country, "España") + + edit_ok = self.client.post(reverse("editar_direccion", args=[new_address.id]), data={ + "full_name": "Comprador Dos Editado", + "address_line_1": "Calle Editada 9", + "city": "Almería", + "postal_code": "04004", + "phone": "600000004", + }) + self.assertEqual(edit_ok.status_code, 302) + new_address.refresh_from_db() + self.assertEqual(new_address.full_name, "Comprador Dos Editado") + + delete_get = self.client.get(reverse("eliminar_direccion", args=[new_address.id])) + self.assertEqual(delete_get.status_code, 302) + delete_post = self.client.post(reverse("eliminar_direccion", args=[new_address.id])) + self.assertEqual(delete_post.status_code, 302) + self.assertFalse(ShippingAddress.objects.filter(id=new_address.id).exists()) + + def test_delete_payment_method_endpoint(self): + self._login() + method = SavedPaymentMethod.objects.create( + user=self.buyer, + method_type=SavedPaymentMethod.TYPE_CARD, + label="Visa 4242", + stripe_payment_method_id="pm_4242", + ) + self.assertEqual(self.client.get(reverse("eliminar_metodo_pago", args=[method.id])).status_code, 302) + with patch("tienda.views.stripe.PaymentMethod.detach"): + response = self.client.post(reverse("eliminar_metodo_pago", args=[method.id])) + self.assertEqual(response.status_code, 302) + self.assertFalse(SavedPaymentMethod.objects.filter(id=method.id).exists()) diff --git a/tienda/views.py b/tienda/views.py index 4b783b7..a44db6b 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -1009,13 +1009,6 @@ def editar_producto(request: HttpRequest, id: int): primary_image_file = request.FILES.get("primary_image") secondary_images_files = request.FILES.getlist("secondary_images") - if stock > 4294967295: - messages.error(request, "No se puede tener mas de 4294967295 de stock.") - categories = Category.objects.all() - return render(request, "tienda/editar_producto.html", { - "categories": categories, - "producto": producto - }) if not all([name, description, price, stock, category_id]): messages.error(request, "Por favor completa todos los campos obligatorios.") categories = Category.objects.all() @@ -1040,6 +1033,13 @@ def editar_producto(request: HttpRequest, id: int): stock = int(stock) if stock < 0: raise ValueError("El stock no puede ser negativo") + if stock > 4294967295: + messages.error(request, "No se puede tener mas de 4294967295 de stock.") + categories = Category.objects.all() + return render(request, "tienda/editar_producto.html", { + "categories": categories, + "producto": producto + }) except ValueError: messages.error(request, "El stock debe ser un número entero válido.") categories = Category.objects.all()