ixg_platform/apps/batches/tests.py
ixgadmin 88eede5d7a Restrict the processing-batch API to ops/admin
Any authenticated user could PATCH a batch, and the write serializer accepted
status, so a buyer could move a qc_fail batch to delivered. The buyer portal
reads batches through the ORM, not this API, so the whole viewset is closed to
ops/admin and the write serializer refuses a hand-set QC status and any
transition out of qc_fail.
2026-09-17 14:48:09 +00:00

92 lines
3.8 KiB
Python

"""The processing-batch API.
The portal screens have their own tests; these pin the API boundary that used to
be missing. ``ProcessingBatchViewSet`` was open to any authenticated user and the
write serializer accepted ``status``, so a buyer could PATCH a failed batch to
``delivered``. The write rules mirror ``apps/ops/forms.py``: a QC status is set by
a QC result, and a failed batch is sticky.
"""
from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.test import APIClient
from .models import ProcessingBatch
User = get_user_model()
class ProcessingBatchAPITests(TestCase):
def setUp(self):
self.ops = User.objects.create_user(
email="batch-api-ops@ixg.local", password="pass1234", role="ops",
full_name="Batch API Ops", is_staff=True,
)
self.buyer = User.objects.create_user(
email="batch-api-buyer@ixg.local", password="pass1234", role="buyer",
full_name="Batch API Buyer", is_staff=True,
)
self.failed = ProcessingBatch.objects.create(
batch_reference="IXG-BATCH-API-001",
status=ProcessingBatch.Status.QC_FAIL,
)
self.client = APIClient()
def test_a_buyer_cannot_patch_a_batch(self):
self.client.force_authenticate(user=self.buyer)
response = self.client.patch(
f"/api/batches/{self.failed.id}/", {"status": "delivered"}, format="json"
)
self.assertEqual(response.status_code, 403)
self.failed.refresh_from_db()
self.assertEqual(self.failed.status, ProcessingBatch.Status.QC_FAIL)
def test_a_buyer_cannot_read_the_batch_api(self):
"""The buyer portal reads batches through the ORM, not this API, so the
API needs no buyer read and is closed to one."""
self.client.force_authenticate(user=self.buyer)
self.assertEqual(self.client.get("/api/batches/").status_code, 403)
def test_ops_can_read_the_batch_api(self):
self.client.force_authenticate(user=self.ops)
self.assertEqual(self.client.get("/api/batches/").status_code, 200)
def test_a_qc_status_cannot_be_hand_set(self):
self.client.force_authenticate(user=self.ops)
journey = ProcessingBatch.objects.create(
batch_reference="IXG-BATCH-API-002",
status=ProcessingBatch.Status.HARVESTED,
)
response = self.client.patch(
f"/api/batches/{journey.id}/", {"status": "qc_pass"}, format="json"
)
self.assertEqual(response.status_code, 400)
journey.refresh_from_db()
self.assertEqual(journey.status, ProcessingBatch.Status.HARVESTED)
def test_a_batch_cannot_be_created_with_a_qc_status(self):
self.client.force_authenticate(user=self.ops)
response = self.client.post(
"/api/batches/",
{"batch_reference": "IXG-BATCH-API-003", "status": "qc_pass"},
format="json",
)
self.assertEqual(response.status_code, 400)
self.assertFalse(
ProcessingBatch.objects.filter(batch_reference="IXG-BATCH-API-003").exists()
)
def test_a_failed_batch_cannot_be_moved_out_of_qc_fail(self):
self.client.force_authenticate(user=self.ops)
response = self.client.patch(
f"/api/batches/{self.failed.id}/", {"status": "delivered"}, format="json"
)
self.assertEqual(response.status_code, 400)
self.failed.refresh_from_db()
self.assertEqual(self.failed.status, ProcessingBatch.Status.QC_FAIL)
def test_a_failed_batch_can_be_saved_with_its_own_status(self):
self.client.force_authenticate(user=self.ops)
response = self.client.patch(
f"/api/batches/{self.failed.id}/", {"status": "qc_fail"}, format="json"
)
self.assertEqual(response.status_code, 200)