10 Commits

Author SHA1 Message Date
4410cc919a Merge pull request 'test(Tryton): add catalog sale test and validate build_tryton_comment' (#44) from fix/EnviarComentarioConDetalleDelPedido into main
Reviewed-on: #44
2026-07-11 18:56:05 -05:00
d1c2b4f1bb test(Tryton): add catalog sale test and validate build_tryton_comment
- Added CatalogSale/CatalogSaleLine to setUp
- Added explicit assertion linking TrytonSale.build_tryton_comment()
  to sale.description
- Rewrote test_send_catalog_sales_to_tryton with proper CatalogSale
  data, validating comment format, description, reference and lines
- Added direct assertion of TrytonCatalogSale.build_tryton_comment()
2026-07-11 14:55:44 -05:00
d57b827e4e Merge pull request 'fix(Tryton): restore send sales test and fix TrytonSale.build_tryton_comment' (#43) from fix/EnviarComentarioConDetalleDelPedido into main
Reviewed-on: #43
2026-07-11 14:38:27 -05:00
1e32cfce7f fix(Tryton): restore send sales test and fix TrytonSale.build_tryton_comment
- Restored test_exportar_ventas_para_tryton.py with full parameter
  validation for client.call (method, context, sale fields, lines)
- Fixed TrytonSale.build_tryton_comment referencing self.catalog_sale
  (nonexistent) instead of self.sale.description
2026-07-11 14:32:41 -05:00
f65b85c7e7 feat: Add custom views to admin registered models 2026-07-05 11:08:33 -05:00
ace5091f8b feat: Update admin product view list. 2026-07-03 19:23:55 -05:00
de7c32b10f Merge pull request 'feat: add script to re-upload catalogue images and force resize' (#42) from feat/reupload-catalogue-images into main
Reviewed-on: #42
2026-07-03 18:38:31 -05:00
mono
b93ee95427 feat: add script to re-upload catalogue images and force resize 2026-07-03 18:26:42 -05:00
4fcc7c2503 fix: CatalogSaleSummary accesible para usuarios 'publico'
- Cambia permission_classes de IsNotPublico a IsAuthenticated
- El usuario publico necesita ver el resumen de su compra en catálogo
2026-06-22 12:49:04 -05:00
5b373bd15f feat: agrega rol 'publico' con UserProfile y permisos diferenciados
- Crea modelo UserProfile (OneToOne con User) con user_type: publico/user/administrator
- Señal post_save que crea UserProfile automáticamente al crear User
- UserSerializer.get_role() lee desde profile.user_type
- Nuevos permisos: IsNotPublico, IsPublico y actualiza IsAdministrator
- IsNotPublico restringe acceso a vistas de gestión (productos, clientes, ventas)
- CatalogSaleView.create permite publico; otras acciones requieren IsNotPublico
- UserProfile inline en admin de Usuarios
- Data migration para backfill de usuarios existentes
2026-06-22 12:27:23 -05:00
16 changed files with 604 additions and 27 deletions

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
import argparse
import getpass
import sys
import requests
TOKEN_URL = "/api/token/"
CATALOGUE_IMAGES_URL = "/don_confiao/api/catalogue_images/"
def get_credentials():
username = input("Usuario: ")
password = getpass.getpass("Contraseña: ")
return username, password
def get_token(domain, username, password):
url = domain.rstrip("/") + TOKEN_URL
response = requests.post(url, json={"username": username, "password": password})
if response.status_code != 200:
print(f"Error al obtener token: {response.status_code} {response.text}", file=sys.stderr)
sys.exit(1)
data = response.json()
return data["access"]
def get_catalogue_images(domain, token):
url = domain.rstrip("/") + CATALOGUE_IMAGES_URL
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
if response.status_code != 200:
print(f"Error al obtener imágenes de catálogo: {response.status_code} {response.text}", file=sys.stderr)
sys.exit(1)
return response.json()
def download_image(image_url):
response = requests.get(image_url)
if response.status_code != 200:
print(f" [ERR] No se pudo descargar imagen: {response.status_code}", file=sys.stderr)
return None
return response.content
def main():
parser = argparse.ArgumentParser(
description="Vuelve a subir cada imagen de catálogo existente para forzar el redimensionado."
)
parser.add_argument("domain", help="Dominio del backend (ej: http://localhost:8000)")
args = parser.parse_args()
username, password = get_credentials()
token = get_token(args.domain, username, password)
print("Token obtenido correctamente.")
images = get_catalogue_images(args.domain, token)
if not images:
print("No hay imágenes de catálogo para procesar.")
return
headers = {"Authorization": f"Bearer {token}"}
updated = 0
errors = 0
for img in images:
ci_id = img["id"]
product_id = img["product"]
image_url = img["image"]
print(f" [{ci_id}] Descargando {image_url} ...")
content = download_image(image_url)
if content is None:
errors += 1
continue
# Determine content type from URL extension
ext = image_url.rsplit(".", 1)[-1].lower() if "." in image_url else "jpg"
mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}.get(ext, "application/octet-stream")
filename = f"image.{ext}"
update_url = args.domain.rstrip("/") + CATALOGUE_IMAGES_URL.rstrip("/") + f"/{ci_id}/"
files = {"image": (filename, content, mime)}
data = {"product": product_id}
response = requests.put(update_url, headers=headers, files=files, data=data)
if response.status_code == 200:
print(f" [OK] Imagen {ci_id} (producto {product_id}) actualizada.")
updated += 1
else:
print(f" [ERR] Imagen {ci_id}: {response.status_code} {response.text}")
errors += 1
print(f"\nResumen: {updated} actualizadas, {errors} errores")
if __name__ == "__main__":
main()

View File

@@ -23,12 +23,51 @@ class CustomerAdmin(admin.ModelAdmin):
)
search_fields = ("name", "email", "phone")
@admin.register(Sale)
class SaleAdmin(admin.ModelAdmin):
list_display = (
"id",
"date",
"customer",
"payment_method",
"external_id",
)
search_fields = ("date", "customer__name", "payment_method", "external_id")
list_filter = ("date", "customer__name", "payment_method")
@admin.register(SaleLine)
class SaleLineAdmin(admin.ModelAdmin):
list_display = ("sale", "product", "quantity", "unit_price")
search_fields = ("sale__id", "product__name")
list_filter = ("sale__date", "product__name")
@admin.register(CatalogSale)
class CatalogSaleAdmin(admin.ModelAdmin):
list_display = (
"id",
"date",
"customer_name",
"customer_phone",
"customer_address",
"pickup_method",
"external_id",
)
search_fields = ("date","customer_name", "customer_phone", "pickup_method","external_id")
list_filter = ("date", "customer_name", "pickup_method")
@admin.register(CatalogSaleLine)
class CatalogSaleLineAdmin(admin.ModelAdmin):
list_display = ("catalog_sale", "product", "quantity", "unit_price")
search_fields = ("catalog_sale__id", "product__name")
list_filter = ("catalog_sale__date", "product__name")
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ("name", "price", "measuring_unit", "external_id")
search_fields = ("id", "name",)
list_filter = ("name", "id")
admin.site.register(Sale)
admin.site.register(SaleLine)
admin.site.register(CatalogSale)
admin.site.register(CatalogSaleLine)
admin.site.register(Product)
admin.site.register(ProductCategory)
admin.site.register(Payment)
admin.site.register(ReconciliationJar)

View File

@@ -5,7 +5,7 @@ from rest_framework.permissions import IsAuthenticated
from ..models.customers import Customer
from ..serializers import CustomerSerializer
from ..permissions import IsAdministrator
from ..permissions import IsAdministrator, IsNotPublico
from ..services.tryton.customers import CustomerTrytonService
from ..services.tryton.client import get_tryton_client
@@ -13,7 +13,7 @@ from ..services.tryton.client import get_tryton_client
class CustomerView(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [IsAuthenticated]
permission_classes = [IsNotPublico]
class CustomersFromTrytonView(APIView):

View File

@@ -5,7 +5,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from ..models.products import Product
from ..serializers import ProductSerializer
from ..permissions import IsAdministrator
from ..permissions import IsAdministrator, IsNotPublico
from ..services.tryton.products import ProductTrytonService
from ..services.tryton.client import get_tryton_client
@@ -17,7 +17,7 @@ class ProductView(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ("list", "retrieve"):
return [AllowAny()]
return [IsAuthenticated()]
return [IsNotPublico()]
def get_queryset(self):
"""

View File

@@ -14,7 +14,7 @@ from ..serializers import (
SaleSummarySerializer,
CatalogSaleSummarySerializer,
)
from ..permissions import IsAdministrator
from ..permissions import IsAdministrator, IsNotPublico
from ..services.tryton.sales import SaleTrytonService
from ..services.tryton.client import get_tryton_client
from ..views import sales_to_tryton_csv
@@ -23,7 +23,7 @@ from ..views import sales_to_tryton_csv
class SaleView(viewsets.ModelViewSet):
queryset = Sale.objects.all()
serializer_class = SaleSerializer
permission_classes = [IsAuthenticated]
permission_classes = [IsNotPublico]
def create(self, request):
data = request.data
@@ -59,11 +59,15 @@ class SaleView(viewsets.ModelViewSet):
class CatalogSaleView(viewsets.ModelViewSet):
queryset = CatalogSale.objects.all()
serializer_class = CatalogSaleSerializer
permission_classes = [IsAuthenticated]
def get_permissions(self):
if self.action == "create":
return [IsAuthenticated()]
return [IsNotPublico()]
class SaleSummary(APIView):
permission_classes = [IsAuthenticated]
permission_classes = [IsNotPublico]
def get(self, request, id):
sale = Sale.objects.get(pk=id)

View File

@@ -1,6 +1,26 @@
from rest_framework.permissions import BasePermission
def _get_user_type(user):
if not user or not user.is_authenticated:
return None
profile = getattr(user, "profile", None)
if profile:
return profile.user_type
return "administrator" if user.is_staff else "user"
class IsAdministrator(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_staff
return _get_user_type(request.user) == "administrator"
class IsNotPublico(BasePermission):
def has_permission(self, request, view):
user_type = _get_user_type(request.user)
return user_type is not None and user_type != "publico"
class IsPublico(BasePermission):
def has_permission(self, request, view):
return _get_user_type(request.user) == "publico"

View File

@@ -43,20 +43,28 @@ class TrytonSale:
"shipment_address": self.sale.customer.address_external_id,
"invoice_address": self.sale.customer.address_external_id,
"currency": TRYTON_COP_CURRENCY,
"comment": self.sale.description or "",
"description": "Metodo pago: " + str(self.sale.payment_method or ""),
"comment": self.build_tryton_comment(),
"description": "Metodo pago: "
+ str(self.sale.payment_method or ""),
"party": self.sale.customer.external_id,
"reference": "don_confiao " + str(self.sale.id),
"sale_date": self._format_date(self.sale.date),
"lines": [
[
"create",
[TrytonLineSale(line).to_tryton() for line in self.lines],
[
TrytonLineSale(line).to_tryton()
for line in self.lines
],
]
],
"self_pick_up": True,
}
def build_tryton_comment(self):
"""Construye el comentario para la venta en Tryton"""
return self.sale.description or ""
class TrytonLineSale:
"""Representa una línea de venta para exportación a Tryton"""
@@ -98,20 +106,36 @@ class TrytonCatalogSale:
"shipment_address": self.catalog_sale.customer.address_external_id,
"invoice_address": self.catalog_sale.customer.address_external_id,
"currency": TRYTON_COP_CURRENCY,
"comment": self.catalog_sale.description or "",
"comment": self.build_tryton_comment(),
"description": "Venta de catálogo",
"party": self.catalog_sale.customer.external_id,
"reference": "don_confiao_catalog " + str(self.catalog_sale.id),
"reference": "don_confiao_catalog "
+ str(self.catalog_sale.id),
"sale_date": self._format_date(self.catalog_sale.date),
"lines": [
[
"create",
[TrytonCatalogSaleLine(line).to_tryton() for line in self.lines],
[
TrytonCatalogSaleLine(line).to_tryton()
for line in self.lines
],
]
],
"self_pick_up": True,
}
def build_tryton_comment(self):
"""Construye el comentario para la venta en Tryton"""
comment = f"Cliente: {self.catalog_sale.customer_name or ''}\n"
comment += f"Teléfono: {self.catalog_sale.customer_phone or ''}\n"
comment += (
f"Dirección: {self.catalog_sale.customer_address or ''}\n"
)
comment += f"Método de recogida: {self.catalog_sale.pickup_method or ''}\n"
return comment
class TrytonCatalogSaleLine:
"""Representa una línea de catalog sale para exportación a Tryton"""
@@ -125,8 +149,12 @@ class TrytonCatalogSaleLine:
def to_tryton(self):
return {
"product": self.catalog_sale_line.product.external_id,
"quantity": self._format_decimal(self.catalog_sale_line.quantity),
"quantity": self._format_decimal(
self.catalog_sale_line.quantity
),
"type": "line",
"unit": self.catalog_sale_line.product.unit_external_id,
"unit_price": self._format_decimal(self.catalog_sale_line.unit_price),
"unit_price": self._format_decimal(
self.catalog_sale_line.unit_price
),
}

View File

@@ -0,0 +1,249 @@
import json
from unittest.mock import patch, MagicMock
from django.test import TestCase
from ..models.sales import Sale, SaleLine, CatalogSale, CatalogSaleLine
from ..models.customers import Customer
from ..models.products import Product
from ..services.tryton.client import TrytonSale, TrytonCatalogSale
from .Mixins import LoginMixin
class TestSendSalesToTryton(TestCase, LoginMixin):
def setUp(self):
self.login()
self.product = Product.objects.create(
name="Panela",
price=5000,
measuring_unit="UNIT",
unit_external_id="1",
external_id="1",
)
self.customer = Customer.objects.create(
name="Camilo",
external_id="1",
address_external_id="307",
)
self.sale = Sale.objects.create(
customer=self.customer,
date="2024-09-02",
payment_method="CASH",
description="un comentario",
)
SaleLine.objects.create(
product=self.product,
quantity=2,
unit_price=3000,
sale=self.sale,
)
SaleLine.objects.create(
product=self.product,
quantity=3,
unit_price=5000,
sale=self.sale,
)
self.catalog_sale = CatalogSale.objects.create(
customer=self.customer,
date="2024-09-02",
customer_name="Camilo",
customer_phone="3001234567",
customer_address="Calle 123",
pickup_method="Recogida en tienda",
)
CatalogSaleLine.objects.create(
catalog_sale=self.catalog_sale,
product=self.product,
quantity=2,
unit_price=3000,
)
CatalogSaleLine.objects.create(
catalog_sale=self.catalog_sale,
product=self.product,
quantity=3,
unit_price=5000,
)
@patch("don_confiao.api.sales.get_tryton_client")
def test_send_sales_to_tryton(self, mock_get_tryton_client):
external_id = "23423"
mock_client = MagicMock()
mock_client.call.return_value = [external_id]
mock_get_tryton_client.return_value = mock_client
url = "/don_confiao/api/enviar_ventas_a_tryton"
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
self.assertEqual(
content, {"successful": [self.sale.id], "failed": []}
)
updated_sale = Sale.objects.get(id=self.sale.id)
self.assertEqual(updated_sale.external_id, external_id)
mock_get_tryton_client.assert_called_once()
mock_client.call.assert_called_once()
call_args = mock_client.call.call_args
method, params = call_args[0]
self.assertEqual(method, "model.sale.sale.create")
tryton_context = params[1]
self.assertEqual(tryton_context, {"company": 1, "shops": [1]})
sale_params = params[0][0]
self.assertEqual(sale_params["company"], 1)
self.assertEqual(sale_params["shipment_address"], "307")
self.assertEqual(sale_params["invoice_address"], "307")
self.assertEqual(sale_params["currency"], 31)
self.assertEqual(sale_params["comment"], "un comentario")
self.assertEqual(sale_params["description"], "Metodo pago: CASH")
self.assertEqual(sale_params["party"], "1")
self.assertEqual(
sale_params["reference"], f"don_confiao {self.sale.id}"
)
self.assertEqual(
sale_params["sale_date"],
{"__class__": "date", "year": 2024, "month": 9, "day": 2},
)
self.assertTrue(sale_params["self_pick_up"])
sale_tryton = TrytonSale(
self.sale, SaleLine.objects.filter(sale=self.sale)
)
self.assertEqual(
sale_tryton.build_tryton_comment(), self.sale.description
)
lines_block = sale_params["lines"]
self.assertEqual(len(lines_block), 1)
self.assertEqual(lines_block[0][0], "create")
lines = lines_block[0][1]
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0]["product"], "1")
self.assertEqual(
lines[0]["quantity"],
{"__class__": "Decimal", "decimal": "2.00"},
)
self.assertEqual(lines[0]["type"], "line")
self.assertEqual(lines[0]["unit"], "1")
self.assertEqual(
lines[0]["unit_price"],
{"__class__": "Decimal", "decimal": "3000.00"},
)
self.assertEqual(lines[1]["product"], "1")
self.assertEqual(
lines[1]["quantity"],
{"__class__": "Decimal", "decimal": "3.00"},
)
self.assertEqual(lines[1]["type"], "line")
self.assertEqual(lines[1]["unit"], "1")
self.assertEqual(
lines[1]["unit_price"],
{"__class__": "Decimal", "decimal": "5000.00"},
)
@patch("don_confiao.api.sales.get_tryton_client")
def test_send_catalog_sales_to_tryton(self, mock_get_tryton_client):
external_id = "23423"
mock_client = MagicMock()
mock_client.call.return_value = [external_id]
mock_get_tryton_client.return_value = mock_client
url = "/don_confiao/api/enviar_catalog_sales_a_tryton"
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
self.assertEqual(
content,
{"successful": [self.catalog_sale.id], "failed": []},
)
updated = CatalogSale.objects.get(id=self.catalog_sale.id)
self.assertEqual(updated.external_id, external_id)
mock_get_tryton_client.assert_called_once()
mock_client.call.assert_called_once()
call_args = mock_client.call.call_args
method, params = call_args[0]
self.assertEqual(method, "model.sale.sale.create")
tryton_context = params[1]
self.assertEqual(tryton_context, {"company": 1, "shops": [1]})
sale_params = params[0][0]
self.assertEqual(sale_params["company"], 1)
self.assertEqual(sale_params["shipment_address"], "307")
self.assertEqual(sale_params["invoice_address"], "307")
self.assertEqual(sale_params["currency"], 31)
self.assertEqual(sale_params["description"], "Venta de catálogo")
self.assertEqual(sale_params["party"], "1")
self.assertEqual(
sale_params["reference"],
f"don_confiao_catalog {self.catalog_sale.id}",
)
self.assertEqual(
sale_params["sale_date"],
{"__class__": "date", "year": 2024, "month": 9, "day": 2},
)
self.assertTrue(sale_params["self_pick_up"])
expected_comment = (
"Cliente: Camilo\n"
"Teléfono: 3001234567\n"
"Dirección: Calle 123\n"
"Método de recogida: Recogida en tienda\n"
)
self.assertEqual(sale_params["comment"], expected_comment)
catalog_tryton = TrytonCatalogSale(
self.catalog_sale,
CatalogSaleLine.objects.filter(
catalog_sale=self.catalog_sale
),
)
self.assertEqual(
catalog_tryton.build_tryton_comment(), expected_comment
)
lines_block = sale_params["lines"]
self.assertEqual(len(lines_block), 1)
self.assertEqual(lines_block[0][0], "create")
lines = lines_block[0][1]
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0]["product"], "1")
self.assertEqual(
lines[0]["quantity"],
{"__class__": "Decimal", "decimal": "2.00"},
)
self.assertEqual(lines[0]["type"], "line")
self.assertEqual(lines[0]["unit"], "1")
self.assertEqual(
lines[0]["unit_price"],
{"__class__": "Decimal", "decimal": "3000.00"},
)
self.assertEqual(lines[1]["product"], "1")
self.assertEqual(
lines[1]["quantity"],
{"__class__": "Decimal", "decimal": "3.00"},
)
self.assertEqual(lines[1]["type"], "line")
self.assertEqual(lines[1]["unit"], "1")
self.assertEqual(
lines[1]["unit_price"],
{"__class__": "Decimal", "decimal": "5000.00"},
)

View File

@@ -1,3 +1,23 @@
from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
# Register your models here.
from .models import UserProfile
class UserProfileInline(admin.StackedInline):
model = UserProfile
can_delete = False
class CustomUserAdmin(UserAdmin):
inlines = [UserProfileInline]
def get_inline_instances(self, request, obj=None):
if not obj:
return []
return super().get_inline_instances(request, obj)
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

View File

@@ -3,3 +3,6 @@ from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals # noqa

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0.6 on 2026-06-22 17:26
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_type', models.CharField(choices=[('publico', 'Público'), ('user', 'Usuario'), ('administrator', 'Administrador')], default='user', max_length=20)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,24 @@
from django.db import migrations
def backfill_user_profiles(apps, schema_editor):
User = apps.get_model("auth", "User")
UserProfile = apps.get_model("users", "UserProfile")
for user in User.objects.all():
UserProfile.objects.get_or_create(
user=user,
defaults={
"user_type": "administrator" if user.is_staff else "user",
},
)
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.RunPython(backfill_user_profiles, migrations.RunPython.noop),
]

View File

@@ -1,3 +1,19 @@
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class UserProfile(models.Model):
USER_TYPE_CHOICES = [
("publico", "Público"),
("user", "Usuario"),
("administrator", "Administrador"),
]
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="profile"
)
user_type = models.CharField(
max_length=20, choices=USER_TYPE_CHOICES, default="user"
)
def __str__(self):
return f"{self.user.username} - {self.user_type}"

View File

@@ -1,13 +1,25 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from .models import UserProfile
class UserSerializer(serializers.ModelSerializer):
role = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'role')
fields = (
"id",
"username",
"email",
"first_name",
"last_name",
"role",
)
def get_role(self, obj):
return 'administrator' if obj.is_staff else 'user'
try:
return obj.profile.user_type
except UserProfile.DoesNotExist:
return "administrator" if obj.is_staff else "user"

View File

@@ -0,0 +1,12 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import UserProfile
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
user_type = "administrator" if instance.is_staff else "user"
UserProfile.objects.create(user=instance, user_type=user_type)

View File

@@ -4,6 +4,8 @@ from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from .models import UserProfile
class MeEndpointTests(TestCase):
def setUp(self):
@@ -49,7 +51,6 @@ class MeEndpointTests(TestCase):
username='regular',
email='regular@example.com',
password='regularpass',
is_staff=False
)
refresh = RefreshToken.for_user(regular_user)
@@ -87,6 +88,31 @@ class MeEndpointTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['role'], 'administrator')
def test_publico_user_role_is_publico(self):
"""
Verifica que un usuario con user_type='publico' recibe role 'publico'.
"""
publico_user = User.objects.create_user(
username='publico',
email='publico@example.com',
password='publicopass',
)
profile = UserProfile.objects.get(user=publico_user)
profile.user_type = 'publico'
profile.save()
refresh = RefreshToken.for_user(publico_user)
access_token = str(refresh.access_token)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
url = reverse('current-user')
response = client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['role'], 'publico')
def test_me_endpoint_requires_authentication(self):
"""
Sin token el endpoint debe devolver 401 Unauthorized.