Fix shipment API tests and implement state transitions

This commit is contained in:
Jan Felix Wiebe 2025-04-11 21:46:26 +02:00
parent 0e6ebc387f
commit 6a5d954980
5 changed files with 433 additions and 0 deletions

View file

@ -30,6 +30,7 @@ urlpatterns = [
path('api/2/', include('mail.api_v2')), path('api/2/', include('mail.api_v2')),
path('api/2/', include('notify_sessions.api_v2')), path('api/2/', include('notify_sessions.api_v2')),
path('api/2/', include('authentication.api_v2')), path('api/2/', include('authentication.api_v2')),
path('api/2/', include('shipments.api_v2')),
path('api/', get_info), path('api/', get_info),
path('', include('django_prometheus.urls')), path('', include('django_prometheus.urls')),
] ]

View file

@ -0,0 +1,108 @@
from django.urls import re_path
from rest_framework import routers, viewsets, status
from rest_framework.decorators import api_view, permission_classes, action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import serializers
from .models import Shipment
from inventory.models import Item
from tickets.models import IssueThread
class ShipmentSerializer(serializers.ModelSerializer):
related_items = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Item.objects.all(),
required=False
)
related_tickets = serializers.PrimaryKeyRelatedField(
many=True,
queryset=IssueThread.objects.all(),
required=False
)
class Meta:
model = Shipment
fields = [
'id', 'public_secret', 'state', 'review_requirement',
'recipient_name', 'address_supplements', 'street_address',
'city', 'state_province', 'postal_code', 'country',
'related_items', 'related_tickets',
'created_at', 'updated_at', 'shipped_at', 'completed_at'
]
read_only_fields = ['id', 'public_secret', 'created_at', 'updated_at', 'shipped_at', 'completed_at']
class ShipmentViewSet(viewsets.ModelViewSet):
serializer_class = ShipmentSerializer
queryset = Shipment.objects.all()
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
def add_items(self, request, pk=None):
shipment = self.get_object()
review_requirement = request.data.get('review_requirement')
item_ids = request.data.get('item_ids', [])
try:
items = Item.objects.filter(id__in=item_ids)
shipment.related_items.add(*items)
shipment.add_items(review_requirement)
return Response(ShipmentSerializer(shipment).data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def approve(self, request, pk=None):
shipment = self.get_object()
try:
shipment.approve()
return Response(ShipmentSerializer(shipment).data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def reject(self, request, pk=None):
shipment = self.get_object()
try:
shipment.reject()
return Response(ShipmentSerializer(shipment).data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def mark_shipped(self, request, pk=None):
shipment = self.get_object()
try:
# Validate state transition before attempting to mark as shipped
if shipment.state != Shipment.READY_FOR_SHIPPING:
return Response(
{'error': f'Invalid state transition: Cannot mark shipment as shipped from {shipment.state} state. Shipment must be in READY_FOR_SHIPPING state.'},
status=status.HTTP_400_BAD_REQUEST
)
shipment.mark_shipped()
return Response(ShipmentSerializer(shipment).data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def mark_completed(self, request, pk=None):
shipment = self.get_object()
try:
# Validate state transition before attempting to mark as completed
if shipment.state != Shipment.SHIPPED:
return Response(
{'error': f'Invalid state transition: Cannot mark shipment as completed from {shipment.state} state. Shipment must be in SHIPPED state.'},
status=status.HTTP_400_BAD_REQUEST
)
shipment.mark_completed()
return Response(ShipmentSerializer(shipment).data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
router = routers.SimpleRouter()
router.register(r'shipments', ShipmentViewSet, basename='shipments')
urlpatterns = router.urls

View file

View file

View file

@ -0,0 +1,324 @@
from django.test import TestCase, Client
from django.contrib.auth.models import Permission
from knox.models import AuthToken
from django.utils import timezone
from authentication.models import ExtendedUser
from inventory.models import Event, Item, Container
from tickets.models import IssueThread
from shipments.models import Shipment
class ShipmentApiTest(TestCase):
def setUp(self):
super().setUp()
# Create a test user with all permissions
self.user = ExtendedUser.objects.create_user('testuser', 'test@example.com', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.user.save()
# Create authentication token
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
# Create test event and container
self.event = Event.objects.create(slug='test-event', name='Test Event')
self.container = Container.objects.create(name='Test Container')
# Create test items with container
self.item1 = Item.objects.create(description='Test Item 1', event=self.event, container=self.container)
self.item2 = Item.objects.create(description='Test Item 2', event=self.event, container=self.container)
# Create test ticket
self.ticket = IssueThread.objects.create(name='Test Ticket')
def test_list_shipments_empty(self):
"""Test listing shipments when none exist"""
response = self.client.get('/api/2/shipments/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_create_shipment(self):
"""Test creating a new shipment"""
data = {
'recipient_name': 'John Smith',
'street_address': '123 Oxford Street',
'city': 'London',
'state_province': 'Greater London',
'postal_code': 'W1D 2HG',
'country': 'United Kingdom',
'related_items': [],
'related_tickets': []
}
response = self.client.post('/api/2/shipments/', data, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['recipient_name'], 'John Smith')
self.assertEqual(response.json()['state'], Shipment.CREATED)
self.assertEqual(response.json()['review_requirement'], None)
# Retrieve the created shipment and verify its details
shipment_id = response.json()['id']
get_response = self.client.get(f'/api/2/shipments/{shipment_id}/')
self.assertEqual(get_response.status_code, 200)
self.assertEqual(get_response.json()['id'], shipment_id)
self.assertEqual(get_response.json()['recipient_name'], 'John Smith')
self.assertEqual(get_response.json()['street_address'], '123 Oxford Street')
self.assertEqual(get_response.json()['city'], 'London')
self.assertEqual(get_response.json()['state_province'], 'Greater London')
self.assertEqual(get_response.json()['postal_code'], 'W1D 2HG')
self.assertEqual(get_response.json()['country'], 'United Kingdom')
self.assertEqual(get_response.json()['state'], Shipment.CREATED)
self.assertEqual(get_response.json()['review_requirement'], None)
def test_get_shipment(self):
"""Test retrieving a specific shipment"""
# Create a shipment first
shipment = Shipment.objects.create(
recipient_name='Maria Garcia',
street_address='Calle Gran Via 28',
city='Madrid',
state_province='Madrid',
postal_code='28013',
country='Spain'
)
response = self.client.get(f'/api/2/shipments/{shipment.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['id'], shipment.id)
self.assertEqual(response.json()['recipient_name'], 'Maria Garcia')
def test_update_shipment(self):
"""Test updating a shipment"""
# Create a shipment first
shipment = Shipment.objects.create(
recipient_name='Hans Mueller',
street_address='Kurfürstendamm 216',
city='Berlin',
state_province='Berlin',
postal_code='10719',
country='Germany'
)
data = {
'recipient_name': 'Hans-Jürgen Mueller',
'address_supplements': 'Apartment 4B'
}
response = self.client.patch(f'/api/2/shipments/{shipment.id}/', data, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['recipient_name'], 'Hans-Jürgen Mueller')
self.assertEqual(response.json()['address_supplements'], 'Apartment 4B')
def test_delete_shipment(self):
"""Test deleting a shipment"""
# Create a shipment first
shipment = Shipment.objects.create(
recipient_name='Sophie Dubois',
street_address='15 Rue de Rivoli',
city='Paris',
state_province='Île-de-France',
postal_code='75001',
country='France'
)
response = self.client.delete(f'/api/2/shipments/{shipment.id}/')
self.assertEqual(response.status_code, 204)
# Verify it's soft deleted - use all_objects manager to see deleted items
self.assertTrue(Shipment.all_objects.filter(id=shipment.id).exists())
# Check if it's marked as deleted
shipment.refresh_from_db()
self.assertIsNotNone(shipment.deleted_at)
def test_add_items(self):
"""Test adding items to a shipment"""
# Create a shipment first
shipment = Shipment.objects.create(
recipient_name='Alessandro Romano',
street_address='Via del Corso 123',
city='Rome',
state_province='Lazio',
postal_code='00186',
country='Italy'
)
data = {
'item_ids': [self.item1.id, self.item2.id],
'review_requirement': Shipment.REVIEW_ITEMS
}
response = self.client.post(f'/api/2/shipments/{shipment.id}/add_items/', data, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW)
self.assertEqual(response.json()['review_requirement'], Shipment.REVIEW_ITEMS)
# Verify items were added
shipment.refresh_from_db()
self.assertEqual(shipment.related_items.count(), 2)
def test_approve_shipment(self):
"""Test approving a shipment"""
# Create a shipment and add items
shipment = Shipment.objects.create(
recipient_name='Yuki Tanaka',
street_address='1-1-1 Shibuya',
city='Tokyo',
state_province='Tokyo',
postal_code='150-0002',
country='Japan'
)
shipment.related_items.add(self.item1)
shipment.review_requirement = Shipment.REVIEW_ITEMS
shipment.state = Shipment.PENDING_REVIEW
shipment.save()
response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING)
def test_reject_shipment(self):
"""Test rejecting a shipment"""
# Create a shipment and add items
shipment = Shipment.objects.create(
recipient_name='Carlos Rodriguez',
street_address='Avenida Insurgentes Sur 1602',
city='Mexico City',
state_province='CDMX',
postal_code='03940',
country='Mexico'
)
shipment.related_items.add(self.item1)
shipment.review_requirement = Shipment.REVIEW_ITEMS
shipment.state = Shipment.PENDING_REVIEW
shipment.save()
response = self.client.post(f'/api/2/shipments/{shipment.id}/reject/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], Shipment.REJECTED)
def test_mark_shipped(self):
"""Test marking a shipment as shipped"""
# Create a shipment and set it to READY_FOR_SHIPPING
shipment = Shipment.objects.create(
recipient_name='Anna Kowalski',
street_address='ul. Marszałkowska 126',
city='Warsaw',
state_province='Mazowieckie',
postal_code='00-008',
country='Poland',
state=Shipment.READY_FOR_SHIPPING
)
shipment.related_items.add(self.item1)
response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_shipped/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], Shipment.SHIPPED)
self.assertIsNotNone(response.json()['shipped_at'])
def test_mark_completed(self):
"""Test marking a shipment as completed"""
# Create a shipment and set it to SHIPPED
shipment = Shipment.objects.create(
recipient_name='Lars Andersen',
street_address='Strøget 1',
city='Copenhagen',
state_province='Capital Region',
postal_code='1095',
country='Denmark',
state=Shipment.SHIPPED,
shipped_at=timezone.now()
)
shipment.related_items.add(self.item1)
response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_completed/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['state'], Shipment.COMPLETED)
self.assertIsNotNone(response.json()['completed_at'])
def test_invalid_state_transition(self):
"""Test that invalid state transitions are rejected"""
# Create a shipment in CREATED state
shipment = Shipment.objects.create(
recipient_name='Ahmed Hassan',
street_address='26 July Street',
city='Cairo',
state_province='Cairo Governorate',
postal_code='11511',
country='Egypt'
)
# Try to mark it as shipped directly (invalid transition)
response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_shipped/')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json())
self.assertIn('Invalid state transition', response.json()['error'])
# Verify state hasn't changed
shipment.refresh_from_db()
self.assertEqual(shipment.state, Shipment.CREATED)
def test_add_items_without_review_requirement(self):
"""Test adding items without specifying review requirement"""
# Create a shipment first
shipment = Shipment.objects.create(
recipient_name='Wei Chen',
street_address='88 Century Avenue',
city='Shanghai',
state_province='Shanghai',
postal_code='200120',
country='China'
)
data = {
'item_ids': [self.item1.id, self.item2.id]
}
response = self.client.post(f'/api/2/shipments/{shipment.id}/add_items/', data, content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json())
# Verify state hasn't changed
shipment.refresh_from_db()
self.assertEqual(shipment.state, Shipment.CREATED)
def test_approve_without_items(self):
"""Test approving a shipment without items"""
# Create a shipment with REVIEW_ITEMS requirement
shipment = Shipment.objects.create(
recipient_name='Raj Patel',
street_address='123 MG Road',
city='Mumbai',
state_province='Maharashtra',
postal_code='400001',
country='India',
state=Shipment.PENDING_REVIEW,
review_requirement=Shipment.REVIEW_ITEMS
)
response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json())
# Verify state hasn't changed
shipment.refresh_from_db()
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)
def test_approve_without_address(self):
"""Test approving a shipment without complete address"""
# Create a shipment with REVIEW_ADDRESS requirement
shipment = Shipment.objects.create(
recipient_name='Sofia Oliveira',
street_address='Avenida Paulista 1000',
city='São Paulo',
state_province='São Paulo',
postal_code='01310-100',
country='', # Missing country
state=Shipment.PENDING_REVIEW,
review_requirement=Shipment.REVIEW_ADDRESS
)
shipment.related_items.add(self.item1)
response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/')
self.assertEqual(response.status_code, 400)
self.assertIn('error', response.json())
# Verify state hasn't changed
shipment.refresh_from_db()
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)