Files
dashboard-cpsp/backend/apps/operations/tests_karung_web.py
T

169 lines
6.5 KiB
Python

from datetime import date
from unittest.mock import patch
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from apps.accounts.models import User
from apps.farms.models import Cycle, Kandang, Site
from apps.operations.services.karung_web import (
KarungWebError,
fetch_iot_masuk_for_date,
initial_balance_accuracy,
masuk_source_for_kandang,
request_karung,
source_for_kandang,
)
ACTIVE_SECTIONS_SAMPLE = {
"sections": {
"today": {
"date": "2026-09-02",
"masuk": [
{"karung": 192, "source": "K1 - In"},
{"karung": 2, "source": "K1 - Out"},
{"karung": 120, "source": "K2 - In"},
{"karung": 5, "source": "K2 - Out"},
],
"tuang": [
{"karung": 7, "source": "K1 - Use"},
{"karung": 9, "source": "K2 - Use"},
],
"totals": {"masuk": 312, "tuang": 16},
}
},
"cycle": {"saldo_awal": 240},
}
COMBINED_SAMPLE = {
"masuk": {
"history": [
{"counter_value": 160, "date": "2026-09-02", "id": 17, "source": "K1 - In"},
{"counter_value": 80, "date": "2026-09-02", "id": 19, "source": "K2 - In"},
{"counter_value": 112, "date": "2026-08-27", "id": 9, "source": "K1 - In"},
{"counter_value": 32, "date": "2026-08-27", "id": 11, "source": "K2 - In"},
],
"today": [
{"karung": 160, "source": "K1 - In"},
{"karung": 80, "source": "K2 - In"},
],
}
}
class MasukSourceForKandangTests(TestCase):
def test_maps_kandang_number_to_iot_source(self):
self.assertEqual(masuk_source_for_kandang("Kandang 1"), "K1 - In")
self.assertEqual(masuk_source_for_kandang("Kandang 2"), "K2 - In")
self.assertEqual(source_for_kandang("Kandang 2", kind="Use"), "K2 - Use")
self.assertEqual(source_for_kandang("Kandang 2", kind="Out"), "K2 - Out")
def test_rejects_kandang_name_without_number(self):
with self.assertRaises(ValidationError):
masuk_source_for_kandang("Utara")
class InitialBalanceAccuracyTests(TestCase):
def test_returns_ratio_when_both_values_positive(self):
self.assertEqual(initial_balance_accuracy(120, 115), 95.8)
def test_returns_100_when_both_zero(self):
self.assertEqual(initial_balance_accuracy(0, 0), 100.0)
def test_returns_none_when_iot_missing(self):
self.assertIsNone(initial_balance_accuracy(120, None))
@override_settings(KARUNG_WEB_ADMIN_BASE_URL="http://karung.internal")
class KarungWebCombinedTests(TestCase):
@patch("apps.operations.services.karung_web.KarungWebClient.fetch_combined")
def test_fetch_iot_masuk_for_date_uses_kandang_specific_history_row(self, mock_fetch):
mock_fetch.return_value = COMBINED_SAMPLE
result = fetch_iot_masuk_for_date(date(2026, 8, 27), kandang_name="Kandang 2")
self.assertEqual(result, 32)
@patch("apps.operations.services.karung_web.KarungWebClient.fetch_combined")
def test_fetch_iot_masuk_for_date_matches_source_case_insensitively(self, mock_fetch):
mock_fetch.return_value = {
"masuk": {
"history": [
{"counter_value": 99, "date": "2026-08-27", "source": "K1 - in"},
]
}
}
result = fetch_iot_masuk_for_date(date(2026, 8, 27), kandang_name="Kandang 1")
self.assertEqual(result, 99)
@patch("apps.operations.services.karung_web.date")
@patch("apps.operations.services.karung_web.KarungWebClient.fetch_combined")
def test_fetch_iot_masuk_for_date_falls_back_to_today_live_rows(self, mock_fetch, mock_date):
mock_date.today.return_value = date(2026, 9, 2)
mock_fetch.return_value = {"masuk": {"history": [], "today": COMBINED_SAMPLE["masuk"]["today"]}}
result = fetch_iot_masuk_for_date(date(2026, 9, 2), kandang_name="Kandang 1")
self.assertEqual(result, 160)
@patch("apps.operations.services.karung_web.KarungWebClient.fetch_combined")
def test_fetch_iot_masuk_for_date_returns_none_when_day_missing(self, mock_fetch):
mock_fetch.return_value = COMBINED_SAMPLE
result = fetch_iot_masuk_for_date(date(2026, 8, 26), kandang_name="Kandang 1")
self.assertIsNone(result)
@patch(
"apps.operations.services.karung_web.KarungWebClient.fetch_combined",
side_effect=KarungWebError("upstream down"),
)
def test_fetch_iot_masuk_for_date_propagates_upstream_errors(self, _mock_fetch):
with self.assertRaises(KarungWebError):
fetch_iot_masuk_for_date(date(2026, 8, 27), kandang_name="Kandang 1")
@override_settings(KARUNG_WEB_ADMIN_BASE_URL="http://karung.internal")
class RequestKarungTests(TestCase):
def setUp(self):
user = User.objects.create_user(user_name="tester", password="secret")
site = Site.objects.create(site_name="Sukawarna", user=user)
self.kandang1 = Kandang.objects.create(kandang_name="Kandang 1", site=site)
self.kandang2 = Kandang.objects.create(kandang_name="Kandang 2", site=site)
self.cycle1 = Cycle.objects.create(
kandang=self.kandang1,
total_days=35,
start_date=date(2026, 9, 2),
end_date=date(2026, 10, 5),
doc_in_weight=40,
doc_in_count=10000,
status=Cycle.STATUS_ACTIVE,
)
self.cycle2 = Cycle.objects.create(
kandang=self.kandang2,
total_days=35,
start_date=date(2026, 9, 2),
end_date=date(2026, 10, 5),
doc_in_weight=40,
doc_in_count=10000,
status=Cycle.STATUS_ACTIVE,
)
@patch("apps.operations.services.karung_web.KarungWebClient.fetch_active_sections")
def test_request_karung_uses_per_kandang_sources_not_aggregate_totals(self, mock_fetch):
mock_fetch.return_value = ACTIVE_SECTIONS_SAMPLE
feed_sack1, summary1 = request_karung(self.cycle1, section="today")
feed_sack2, summary2 = request_karung(self.cycle2, section="today")
self.assertEqual(feed_sack1.in_today, 192)
self.assertEqual(feed_sack1.feed_use_today, 7)
self.assertEqual(feed_sack1.out_today, 2)
self.assertEqual(feed_sack2.in_today, 120)
self.assertEqual(feed_sack2.feed_use_today, 9)
self.assertEqual(feed_sack2.out_today, 5)
self.assertEqual(summary1["kandang_totals"]["in_today"], 192)
self.assertEqual(summary2["kandang_totals"]["in_today"], 120)