Some checks are pending
CI / Crawler (Python ${{ matrix.python-version }}) (3.10) (push) Waiting to run
CI / Crawler (Python ${{ matrix.python-version }}) (3.11) (push) Waiting to run
CI / API (Python 3.11) (push) Waiting to run
CI / Database migration (push) Waiting to run
CI / App web build (Node 20) (push) Waiting to run
171 lines
5.6 KiB
Python
171 lines
5.6 KiB
Python
import unittest
|
|
from datetime import date
|
|
from unittest.mock import patch
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from app.routers import stats
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, data=None):
|
|
self.data = data or []
|
|
|
|
|
|
class FakeQuery:
|
|
def __init__(self, db, table_name):
|
|
self.db = db
|
|
self.table_name = table_name
|
|
self.filters = []
|
|
self.db.queries.append(self)
|
|
|
|
def select(self, _columns):
|
|
return self
|
|
|
|
def eq(self, column, value):
|
|
self.filters.append(("eq", column, value))
|
|
return self
|
|
|
|
def gte(self, column, value):
|
|
self.filters.append(("gte", column, value))
|
|
return self
|
|
|
|
def lte(self, column, value):
|
|
self.filters.append(("lte", column, value))
|
|
return self
|
|
|
|
def lt(self, column, value):
|
|
self.filters.append(("lt", column, value))
|
|
return self
|
|
|
|
def order(self, _column, desc=False):
|
|
return self
|
|
|
|
def limit(self, _count):
|
|
return self
|
|
|
|
def execute(self):
|
|
if self.table_name == "plants":
|
|
if not self.db.plant_exists:
|
|
return FakeResponse()
|
|
return FakeResponse([{"id": "plant-a", "name": "1호기", "capacity": 100}])
|
|
if self.table_name == "solar_logs":
|
|
return FakeResponse(self.db.solar_logs)
|
|
if self.table_name == "monthly_stats":
|
|
return FakeResponse(self.db.monthly_stats)
|
|
return FakeResponse()
|
|
|
|
|
|
class FakeDb:
|
|
def __init__(self, solar_logs=None, monthly_stats=None, plant_exists=True):
|
|
self.solar_logs = solar_logs or []
|
|
self.monthly_stats = monthly_stats or []
|
|
self.plant_exists = plant_exists
|
|
self.queries = []
|
|
|
|
def table(self, table_name):
|
|
return FakeQuery(self, table_name)
|
|
|
|
def latest_query(self, table_name):
|
|
return next(query for query in reversed(self.queries) if query.table_name == table_name)
|
|
|
|
|
|
class StatsTimezoneTest(unittest.TestCase):
|
|
def test_comparison_today_uses_kst_utc_half_open_bounds(self):
|
|
db = FakeDb([{"plant_id": "plant-a", "today_kwh": 123.4}])
|
|
|
|
with patch.object(stats, "today_kst", return_value=date(2026, 1, 1)):
|
|
result = stats.get_all_plants_comparison(
|
|
period="day", date=None, year=None, month=None, db=db
|
|
)
|
|
|
|
query = db.latest_query("solar_logs")
|
|
self.assertIn(("gte", "created_at", "2025-12-31T15:00:00+00:00"), query.filters)
|
|
self.assertIn(("lt", "created_at", "2026-01-01T15:00:00+00:00"), query.filters)
|
|
self.assertEqual(123.4, result.data[0].generation)
|
|
|
|
def test_plant_today_query_has_upper_bound(self):
|
|
db = FakeDb([{"today_kwh": 77.0}])
|
|
|
|
with patch.object(stats, "today_kst", return_value=date(2026, 8, 6)):
|
|
result = stats.get_plant_stats(
|
|
plant_id="plant-a", period="day", year=2026, month=8, db=db
|
|
)
|
|
|
|
query = db.latest_query("solar_logs")
|
|
self.assertIn(("gte", "created_at", "2026-08-05T15:00:00+00:00"), query.filters)
|
|
self.assertIn(("lt", "created_at", "2026-08-06T15:00:00+00:00"), query.filters)
|
|
self.assertEqual(77.0, result.data[5].value)
|
|
|
|
def test_hourly_stats_map_utc_edges_to_kst_hours(self):
|
|
db = FakeDb([
|
|
{"created_at": "2026-12-30T15:00:00Z", "current_kw": 1, "today_kwh": 1},
|
|
{"created_at": "2026-12-31T14:59:59Z", "current_kw": 2, "today_kwh": 2},
|
|
{"created_at": "2026-12-31T15:00:00Z", "current_kw": 3, "today_kwh": 3},
|
|
])
|
|
|
|
result = stats.get_plant_hourly_stats(
|
|
plant_id="plant-a", date="2026-12-31", db=db
|
|
)
|
|
|
|
query = db.latest_query("solar_logs")
|
|
self.assertIn(("gte", "created_at", "2026-12-30T15:00:00+00:00"), query.filters)
|
|
self.assertIn(("lt", "created_at", "2026-12-31T15:00:00+00:00"), query.filters)
|
|
self.assertTrue(result.data[0].has_data)
|
|
self.assertTrue(result.data[23].has_data)
|
|
self.assertEqual(2, result.count)
|
|
|
|
def test_invalid_hourly_date_remains_400(self):
|
|
with self.assertRaises(HTTPException) as raised:
|
|
stats.get_plant_hourly_stats(
|
|
plant_id="plant-a", date="2026-13-01", db=FakeDb()
|
|
)
|
|
|
|
self.assertEqual(400, raised.exception.status_code)
|
|
|
|
def test_invalid_comparison_date_is_not_replaced_with_today(self):
|
|
db = FakeDb()
|
|
|
|
with self.assertRaises(HTTPException) as raised:
|
|
stats.get_all_plants_comparison(
|
|
period="day", date="2026-8-07", year=None, month=None, db=db
|
|
)
|
|
|
|
self.assertEqual(400, raised.exception.status_code)
|
|
self.assertEqual([], db.queries)
|
|
|
|
def test_unknown_plant_stats_returns_404(self):
|
|
with self.assertRaises(HTTPException) as raised:
|
|
stats.get_plant_stats(
|
|
plant_id="missing", period="day", year=2026, month=8,
|
|
db=FakeDb(plant_exists=False),
|
|
)
|
|
|
|
self.assertEqual(404, raised.exception.status_code)
|
|
|
|
def test_leap_year_uses_366_days(self):
|
|
db = FakeDb(monthly_stats=[{
|
|
"plant_id": "plant-a",
|
|
"total_generation": 36600,
|
|
}])
|
|
|
|
result = stats.get_all_plants_comparison(
|
|
period="year", date="2024-12-31", year=2024, month=None, db=db
|
|
)
|
|
|
|
self.assertEqual(1.0, result.data[0].generation_hours)
|
|
|
|
def test_selected_year_is_used_as_yearly_query_end(self):
|
|
db = FakeDb()
|
|
|
|
stats.get_plant_stats(
|
|
plant_id="plant-a", period="year", year=2024, month=None, db=db
|
|
)
|
|
|
|
query = db.latest_query("monthly_stats")
|
|
self.assertIn(("lte", "month", "2024-12"), query.filters)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|