Files
dashboard-cpsp/backend/apps/farms/tests_pusat_export.py
T

221 lines
8.8 KiB
Python

from datetime import date, timedelta
from unittest.mock import patch
from django.test import TestCase, override_settings
from django.utils import timezone
from rest_framework.test import APIClient
from apps.accounts.models import ApiKey, User
from apps.farms.models import Cycle, Flock, Kandang, Site
from apps.operations.models import AIInsight, ChickenCounting, FeedSacks, IotPanel
SITE_KEY = "site-export-test-key"
@override_settings(DASHBOARD_PUBLISH_HOUR=17, TIME_ZONE="Asia/Jakarta")
class PusatExportTests(TestCase):
def setUp(self):
self.client = APIClient()
self.admin = User.objects.create_user(
user_name="export-admin",
password="x",
status=User.STATUS_SUPERADMIN,
)
api_key, _ = ApiKey.generate(self.admin, "site")
api_key.prefix = SITE_KEY[:8]
api_key.key_hash = ApiKey.hash_key(SITE_KEY)
api_key.save(update_fields=["prefix", "key_hash", "updated_at"])
self.site = Site.objects.create(site_name="ExportSite", user=self.admin)
self.other_site = Site.objects.create(site_name="OtherSite", user=self.admin)
self.kandang = Kandang.objects.create(kandang_name="K1", site=self.site)
self.other_kandang = Kandang.objects.create(kandang_name="K2", site=self.other_site)
self.flock = Flock.objects.create(flock_name="F1", kandang=self.kandang)
self.other_flock = Flock.objects.create(flock_name="F2", kandang=self.other_kandang)
today = timezone.localdate()
self.cycle = Cycle.objects.create(
kandang=self.kandang,
start_date=today - timedelta(days=10),
doc_in_weight=40,
doc_in_count=1000,
status=Cycle.STATUS_ACTIVE,
)
self.other_cycle = Cycle.objects.create(
kandang=self.other_kandang,
start_date=today - timedelta(days=10),
doc_in_weight=40,
doc_in_count=1000,
status=Cycle.STATUS_ACTIVE,
)
# "Today" rows would be hidden by publish cutoff on normal list APIs before 17:00.
FeedSacks.objects.create(cycle=self.cycle, date=today, in_today=1)
FeedSacks.objects.create(cycle=self.other_cycle, date=today, in_today=2)
ChickenCounting.objects.create(
cycle=self.cycle, date=today, total_count=100, mortality_count=1
)
AIInsight.objects.create(
cycle=self.cycle,
kandang=self.kandang,
date=today,
insight_text="ok",
alert="healthy",
topic="fcr",
report_type="page",
report_period="Hari 1",
)
IotPanel.objects.create(
flock=self.flock,
date=today,
timestamp=timezone.now(),
wind_speed=1.0,
humidity=50.0,
water_total=10.0,
average_temperature=28.0,
experience_temperature=27.0,
)
IotPanel.objects.create(
flock=self.other_flock,
date=today,
timestamp=timezone.now(),
wind_speed=1.0,
humidity=50.0,
water_total=10.0,
average_temperature=28.0,
experience_temperature=27.0,
)
def _auth(self):
self.client.credentials(HTTP_X_API_KEY=SITE_KEY)
def test_ops_requires_auth(self):
response = self.client.get("/api/v1/pusat/export/", {"site_id": self.site.pk})
self.assertEqual(response.status_code, 401)
def test_ops_requires_site_id(self):
self._auth()
response = self.client.get("/api/v1/pusat/export/")
self.assertEqual(response.status_code, 400)
def test_ops_unknown_site(self):
self._auth()
response = self.client.get("/api/v1/pusat/export/", {"site_id": "missing-9999"})
self.assertEqual(response.status_code, 400)
def test_ops_cycle_site_mismatch(self):
self._auth()
response = self.client.get(
"/api/v1/pusat/export/",
{"site_id": self.site.pk, "cycle_id": self.other_cycle.pk},
)
self.assertEqual(response.status_code, 400)
def test_ops_export_scopes_and_includes_today(self):
self._auth()
today = timezone.localdate().isoformat()
response = self.client.get(
"/api/v1/pusat/export/",
{"site_id": self.site.pk, "from": today, "to": today},
)
self.assertEqual(response.status_code, 200, response.data)
self.assertEqual(response.data["site_id"], self.site.pk)
self.assertEqual(len(response.data["sites"]), 1)
self.assertNotIn("flocks", response.data["sites"][0]["kandangs"][0])
self.assertNotIn("iot_panels", response.data["data"])
self.assertEqual(response.data["data"]["feed_sacks"]["count"], 1)
self.assertEqual(response.data["data"]["chicken_countings"]["count"], 1)
self.assertEqual(response.data["data"]["ai_insights"]["count"], 1)
self.assertEqual(
response.data["data"]["feed_sacks"]["results"][0]["site_id"], self.site.pk
)
def test_ops_cycle_id_defaults_to_cycle_span(self):
self._auth()
closed = Cycle.objects.create(
kandang=self.kandang,
start_date=date(2026, 1, 1),
end_date=date(2026, 2, 10),
doc_in_weight=40,
doc_in_count=1000,
status=Cycle.STATUS_CLOSED,
)
FeedSacks.objects.create(cycle=closed, date=date(2026, 1, 15), in_today=3)
response = self.client.get(
"/api/v1/pusat/export/",
{"site_id": self.site.pk, "cycle_id": closed.pk},
)
self.assertEqual(response.status_code, 200, response.data)
self.assertEqual(response.data["from"], "2026-01-01")
self.assertEqual(response.data["to"], "2026-02-10")
self.assertEqual(response.data["data"]["feed_sacks"]["count"], 1)
def test_ops_export_includes_initial_balance_iot(self):
self._auth()
today = timezone.localdate()
self.cycle.feed_initial_balance = 120
self.cycle.feed_initial_balance_date = today
self.cycle.save(
update_fields=["feed_initial_balance", "feed_initial_balance_date", "updated_at"]
)
with patch(
"apps.farms.services.pusat_export.pull_initial_balance_iot",
return_value=(115, None),
) as mock_pull:
response = self.client.get(
"/api/v1/pusat/export/",
{"site_id": self.site.pk, "cycle_id": self.cycle.pk},
)
self.assertEqual(response.status_code, 200, response.data)
cycle_row = response.data["sites"][0]["kandangs"][0]["cycles"][0]
self.assertEqual(cycle_row["feed_initial_balance"], 120)
self.assertEqual(cycle_row["feed_initial_balance_date"], today.isoformat())
self.assertEqual(cycle_row["feed_initial_balance_iot"], 115)
self.assertEqual(cycle_row["feed_initial_balance_accuracy"], 95.8)
self.assertIsNone(cycle_row["feed_initial_balance_sync_error"])
mock_pull.assert_called_once_with(self.cycle, today)
def test_ops_export_skips_iot_pull_without_balance_date(self):
self._auth()
with patch(
"apps.farms.services.pusat_export.pull_initial_balance_iot",
return_value=(99, None),
) as mock_pull:
response = self.client.get(
"/api/v1/pusat/export/",
{"site_id": self.site.pk, "cycle_id": self.cycle.pk},
)
self.assertEqual(response.status_code, 200, response.data)
cycle_row = response.data["sites"][0]["kandangs"][0]["cycles"][0]
self.assertIsNone(cycle_row["feed_initial_balance_iot"])
self.assertIsNone(cycle_row["feed_initial_balance_accuracy"])
self.assertIsNone(cycle_row["feed_initial_balance_sync_error"])
mock_pull.assert_not_called()
def test_iot_flock_site_mismatch(self):
self._auth()
response = self.client.get(
"/api/v1/pusat/export/iot/",
{"site_id": self.site.pk, "flock_id": self.other_flock.pk},
)
self.assertEqual(response.status_code, 400)
def test_iot_export_structure_and_panels(self):
self._auth()
today = timezone.localdate().isoformat()
response = self.client.get(
"/api/v1/pusat/export/iot/",
{"site_id": self.site.pk, "from": today, "to": today},
)
self.assertEqual(response.status_code, 200, response.data)
site_node = response.data["sites"][0]
self.assertEqual(site_node["site_id"], self.site.pk)
self.assertIn("flocks", site_node["kandangs"][0])
self.assertNotIn("cycles", site_node["kandangs"][0])
self.assertEqual(response.data["data"]["iot_panels"]["count"], 1)
self.assertEqual(
response.data["data"]["iot_panels"]["results"][0]["site_id"], self.site.pk
)