Fix shipment API tests and implement state transitions
This commit is contained in:
parent
0e6ebc387f
commit
6a5d954980
5 changed files with 433 additions and 0 deletions
|
@ -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')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
0
core/shipments/tests/__init__.py
Normal file
0
core/shipments/tests/__init__.py
Normal file
0
core/shipments/tests/v2/__init__.py
Normal file
0
core/shipments/tests/v2/__init__.py
Normal file
324
core/shipments/tests/v2/test_api.py
Normal file
324
core/shipments/tests/v2/test_api.py
Normal 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)
|
Loading…
Add table
Reference in a new issue