Push the rest
This commit is contained in:
@@ -1,79 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.services.analytics_service import AnalyticsService
|
||||
import pytest
|
||||
|
||||
|
||||
class StubWarehouseClient:
|
||||
def fetch_daily_sales(self) -> pd.DataFrame:
|
||||
today = date.today()
|
||||
rows = []
|
||||
for i in range(120):
|
||||
day = today - timedelta(days=120 - i)
|
||||
rows.append(
|
||||
{
|
||||
"sale_date": day.isoformat(),
|
||||
"revenue": 1000 + (i * 5),
|
||||
"cost": 500 + (i * 2),
|
||||
"quantity": 40 + i,
|
||||
"orders": 5 + (i % 4),
|
||||
"source": "stub",
|
||||
}
|
||||
)
|
||||
return pd.DataFrame(rows)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fetch_product_performance(self) -> pd.DataFrame:
|
||||
return pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"product_id": "A1",
|
||||
"product_name": "Alpha",
|
||||
"category_name": "CatA",
|
||||
"revenue": 12000,
|
||||
"cost": 6000,
|
||||
"quantity": 400,
|
||||
"orders": 150,
|
||||
"source": "stub",
|
||||
},
|
||||
{
|
||||
"product_id": "B1",
|
||||
"product_name": "Beta",
|
||||
"category_name": "CatB",
|
||||
"revenue": 9000,
|
||||
"cost": 8500,
|
||||
"quantity": 300,
|
||||
"orders": 110,
|
||||
"source": "stub",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def fetch_customer_performance(self) -> pd.DataFrame:
|
||||
return pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"customer_id": "C1",
|
||||
"customer_name": "Contoso",
|
||||
"revenue": 15000,
|
||||
"orders": 80,
|
||||
"source": "stub",
|
||||
}
|
||||
]
|
||||
)
|
||||
def _make_pg_factory(session_mock: MagicMock) -> MagicMock:
|
||||
factory = MagicMock()
|
||||
factory.return_value.__enter__ = MagicMock(return_value=session_mock)
|
||||
factory.return_value.__exit__ = MagicMock(return_value=False)
|
||||
return factory
|
||||
|
||||
|
||||
def test_forecast_has_expected_horizon() -> None:
|
||||
service = AnalyticsService(StubWarehouseClient()) # type: ignore[arg-type]
|
||||
forecast = service.get_forecast(horizon_days=15)
|
||||
assert len(forecast) == 15
|
||||
assert "predicted_revenue" in forecast[0]
|
||||
# ---------------------------------------------------------------------------
|
||||
# AW persistence layer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAWPersist:
|
||||
def test_persist_forecast_writes_record(self) -> None:
|
||||
from app.domain.aw import analytics
|
||||
|
||||
session = MagicMock()
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
data = [
|
||||
{"date": "2025-01-01", "predicted_revenue": 1000.0,
|
||||
"lower_bound": 900.0, "upper_bound": 1100.0},
|
||||
]
|
||||
|
||||
with patch.object(analytics, "append_audit"):
|
||||
analytics.persist_forecast(factory, data, horizon_days=30, trigger_source="test")
|
||||
|
||||
session.add.assert_called_once()
|
||||
model = session.add.call_args[0][0]
|
||||
assert model.horizon_days == 30
|
||||
assert model.point_count == 1
|
||||
|
||||
def test_persist_rep_scores_writes_record(self) -> None:
|
||||
from app.domain.aw import analytics
|
||||
|
||||
session = MagicMock()
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
data = [
|
||||
{"rep_name": "Alice", "total_revenue": 100_000.0,
|
||||
"total_orders": 50, "performance_score": 0.92},
|
||||
]
|
||||
|
||||
with patch.object(analytics, "append_audit"):
|
||||
analytics.persist_rep_scores(factory, data, top_n=10, trigger_source="test")
|
||||
|
||||
session.add.assert_called_once()
|
||||
model = session.add.call_args[0][0]
|
||||
assert model.rep_count == 1
|
||||
|
||||
def test_persist_product_demand_writes_record(self) -> None:
|
||||
from app.domain.aw import analytics
|
||||
|
||||
session = MagicMock()
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
data = [{"product_name": "Widget", "total_quantity": 500.0}]
|
||||
|
||||
with patch.object(analytics, "append_audit"):
|
||||
analytics.persist_product_demand(factory, data, top_n=10, trigger_source="test")
|
||||
|
||||
session.add.assert_called_once()
|
||||
|
||||
def test_persist_anomaly_run_writes_record(self) -> None:
|
||||
from app.domain.aw import analytics
|
||||
|
||||
session = MagicMock()
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
data = [
|
||||
{"is_anomaly": True, "date": "2025-01-01", "revenue": 50.0},
|
||||
{"is_anomaly": False, "date": "2025-01-02", "revenue": 1000.0},
|
||||
]
|
||||
|
||||
with patch.object(analytics, "append_audit"):
|
||||
analytics.persist_anomaly_run(factory, data, trigger_source="test")
|
||||
|
||||
session.add.assert_called_once()
|
||||
|
||||
|
||||
def test_rankings_are_sorted() -> None:
|
||||
service = AnalyticsService(StubWarehouseClient()) # type: ignore[arg-type]
|
||||
rankings = service.get_rankings(top_n=2)
|
||||
assert len(rankings) == 2
|
||||
assert rankings[0]["score"] >= rankings[1]["score"]
|
||||
# ---------------------------------------------------------------------------
|
||||
# WWI persistence layer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestWWIPersist:
|
||||
def test_persist_reorder_recommendations_writes_record(self) -> None:
|
||||
from app.domain.wwi import analytics
|
||||
|
||||
session = MagicMock()
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
data = [
|
||||
{"stock_item_key": 1, "stock_item_name": "Widget",
|
||||
"current_stock": 10.0, "urgency": "HIGH"},
|
||||
]
|
||||
|
||||
with patch.object(analytics, "append_audit"):
|
||||
analytics.persist_reorder_recommendations(factory, data, trigger_source="test")
|
||||
|
||||
session.add.assert_called_once()
|
||||
model = session.add.call_args[0][0]
|
||||
assert model.item_count == 1
|
||||
|
||||
def test_persist_supplier_scores_writes_record(self) -> None:
|
||||
from app.domain.wwi import analytics
|
||||
|
||||
session = MagicMock()
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
data = [
|
||||
{"supplier_name": "Acme", "performance_score": 0.87},
|
||||
]
|
||||
|
||||
with patch.object(analytics, "append_audit"):
|
||||
analytics.persist_supplier_scores(factory, data, top_n=10, trigger_source="test")
|
||||
|
||||
session.add.assert_called_once()
|
||||
model = session.add.call_args[0][0]
|
||||
assert model.supplier_count == 1
|
||||
|
||||
def test_generate_stock_events_skips_non_high_urgency(self) -> None:
|
||||
from app.domain.wwi import analytics
|
||||
from app.domain.wwi.models import WWIBusinessEvent
|
||||
|
||||
session = MagicMock()
|
||||
session.query.return_value.filter.return_value.first.return_value = None
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
items = [
|
||||
{"stock_item_key": 1, "stock_item_name": "Widget", "urgency": "LOW",
|
||||
"current_stock": 100.0, "avg_daily_demand": 5.0,
|
||||
"days_until_stockout": 20.0, "recommended_reorder_qty": 50},
|
||||
{"stock_item_key": 2, "stock_item_name": "Gadget", "urgency": "MEDIUM",
|
||||
"current_stock": 50.0, "avg_daily_demand": 3.0,
|
||||
"days_until_stockout": 16.0, "recommended_reorder_qty": 30},
|
||||
]
|
||||
|
||||
analytics.generate_stock_events(factory, items)
|
||||
|
||||
# No events should have been added since neither item is HIGH urgency
|
||||
session.add.assert_not_called()
|
||||
|
||||
def test_generate_stock_events_creates_event_for_high_urgency(self) -> None:
|
||||
from app.domain.wwi import analytics
|
||||
|
||||
session = MagicMock()
|
||||
# Simulate no existing event in the 24h window
|
||||
session.query.return_value.filter.return_value.first.return_value = None
|
||||
factory = _make_pg_factory(session)
|
||||
|
||||
items = [
|
||||
{"stock_item_key": 42, "stock_item_name": "Critical Part", "urgency": "HIGH",
|
||||
"current_stock": 2.0, "avg_daily_demand": 5.0,
|
||||
"days_until_stockout": None, "recommended_reorder_qty": 100},
|
||||
]
|
||||
|
||||
analytics.generate_stock_events(factory, items)
|
||||
|
||||
session.add.assert_called_once()
|
||||
event = session.add.call_args[0][0]
|
||||
assert event.event_type == "LOW_STOCK"
|
||||
assert event.entity_key == "42"
|
||||
assert "immediately" in event.message
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import InternalTokenManager, require_internal_principal
|
||||
|
||||
|
||||
def test_internal_token_round_trip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
settings,
|
||||
"internal_service_shared_secret",
|
||||
"unit-test-shared-secret-key-at-least-32b",
|
||||
)
|
||||
monkeypatch.setattr(settings, "internal_service_token_audience", "bi-internal-test")
|
||||
monkeypatch.setattr(settings, "internal_service_allowed_issuers", "api-gateway")
|
||||
monkeypatch.setattr(settings, "internal_token_clock_skew_seconds", 0)
|
||||
|
||||
manager = InternalTokenManager()
|
||||
token = manager.mint(
|
||||
subject="user-123",
|
||||
scopes=["openid", "profile"],
|
||||
source_service="api-gateway",
|
||||
)
|
||||
|
||||
principal = manager.verify(token)
|
||||
assert principal.subject == "user-123"
|
||||
assert principal.claims["iss"] == "api-gateway"
|
||||
assert principal.claims["typ"] == "internal-service"
|
||||
|
||||
|
||||
def test_internal_token_rejects_untrusted_issuer(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
settings,
|
||||
"internal_service_shared_secret",
|
||||
"unit-test-shared-secret-key-at-least-32b",
|
||||
)
|
||||
monkeypatch.setattr(settings, "internal_service_token_audience", "bi-internal-test")
|
||||
monkeypatch.setattr(settings, "internal_service_allowed_issuers", "api-gateway")
|
||||
monkeypatch.setattr(settings, "internal_token_clock_skew_seconds", 0)
|
||||
|
||||
manager = InternalTokenManager()
|
||||
token = manager.mint(
|
||||
subject="user-123",
|
||||
scopes=["openid"],
|
||||
source_service="analytics",
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
manager.verify(token)
|
||||
assert exc.value.status_code == 401
|
||||
assert exc.value.detail == "Internal token issuer is not allowed."
|
||||
|
||||
|
||||
def test_require_internal_principal_rejects_missing_token(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(settings, "internal_service_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
require_internal_principal(None)
|
||||
assert exc.value.status_code == 401
|
||||
assert exc.value.detail == "Missing x-internal-service-token header."
|
||||
Reference in New Issue
Block a user