Cover access on GET and POST for a buyer, each required field when blank, an ETD after the ETA, a batch that already has a shipment (queried, not blocked), a status change, and an edit updating in place. Add the shipment routes to both OPS_ROUTES sweeps so the detail and edit screens render clean markup, and assert the native date inputs carry ISO values — the defect that left the intake and batch edit dates blank.
98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
"""The shipment API.
|
|
|
|
The portal form and the API must agree: the fields the buyer's page reads are
|
|
required, and a departure after the arrival is refused. The permission boundary
|
|
itself is swept in ``apps/core/tests.py``; these pin the serializer rules so the
|
|
API cannot accept what the ops form refuses.
|
|
"""
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from rest_framework.test import APIClient
|
|
|
|
from apps.batches.models import ProcessingBatch
|
|
|
|
from .models import Shipment
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class ShipmentAPITests(TestCase):
|
|
def setUp(self):
|
|
self.ops = User.objects.create_user(
|
|
email="ship-api-ops@ixg.local", password="pass1234", role="ops",
|
|
full_name="Shipment API Ops", is_staff=True,
|
|
)
|
|
self.buyer = User.objects.create_user(
|
|
email="ship-api-buyer@ixg.local", password="pass1234", role="buyer",
|
|
full_name="Shipment API Buyer", is_staff=True,
|
|
)
|
|
self.batch = ProcessingBatch.objects.create(
|
|
batch_reference="IXG-BATCH-API-SHIP-001", status="ready_to_ship",
|
|
)
|
|
self.shipment = Shipment.objects.create(
|
|
batch=self.batch, status=Shipment.Status.BOOKED,
|
|
vessel_name="MV API", container_ref="IXGU0000001",
|
|
etd_nigeria="2026-09-15", eta_uk="2026-10-15",
|
|
)
|
|
self.client = APIClient()
|
|
|
|
def payload(self, **overrides):
|
|
data = {
|
|
"batch": str(self.batch.pk),
|
|
"status": "booked",
|
|
"vessel_name": "MV API",
|
|
"container_ref": "IXGU0000002",
|
|
"etd_nigeria": "2026-09-20",
|
|
"eta_uk": "2026-10-20",
|
|
}
|
|
data.update(overrides)
|
|
return data
|
|
|
|
def test_a_buyer_cannot_read_or_write_shipments(self):
|
|
self.client.force_authenticate(user=self.buyer)
|
|
self.assertEqual(self.client.get("/api/shipments/").status_code, 403)
|
|
self.assertEqual(
|
|
self.client.post("/api/shipments/", self.payload(), format="json").status_code,
|
|
403,
|
|
)
|
|
|
|
def test_ops_can_create_a_shipment(self):
|
|
self.client.force_authenticate(user=self.ops)
|
|
response = self.client.post("/api/shipments/", self.payload(), format="json")
|
|
self.assertEqual(response.status_code, 201)
|
|
self.assertEqual(Shipment.objects.count(), 2)
|
|
|
|
def test_each_required_field_is_refused_when_blank(self):
|
|
self.client.force_authenticate(user=self.ops)
|
|
for field in (
|
|
"status", "vessel_name", "container_ref", "etd_nigeria", "eta_uk",
|
|
):
|
|
with self.subTest(field=field):
|
|
response = self.client.post(
|
|
"/api/shipments/", self.payload(**{field: ""}), format="json"
|
|
)
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertIn(field, response.json())
|
|
self.assertEqual(Shipment.objects.count(), 1)
|
|
|
|
def test_a_departure_after_the_arrival_is_refused(self):
|
|
self.client.force_authenticate(user=self.ops)
|
|
response = self.client.post(
|
|
"/api/shipments/",
|
|
self.payload(etd_nigeria="2026-11-01", eta_uk="2026-10-01"),
|
|
format="json",
|
|
)
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertIn("eta_uk", response.json())
|
|
self.assertEqual(Shipment.objects.count(), 1)
|
|
|
|
def test_a_partial_status_change_does_not_require_the_other_fields(self):
|
|
self.client.force_authenticate(user=self.ops)
|
|
response = self.client.patch(
|
|
f"/api/shipments/{self.shipment.pk}/",
|
|
{"status": "in_transit"},
|
|
format="json",
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.shipment.refresh_from_db()
|
|
self.assertEqual(self.shipment.status, Shipment.Status.IN_TRANSIT)
|