test: reorganize core suite into explicit domain files
This commit is contained in:
54
tests/stats/test_stats_account_data.py
Normal file
54
tests/stats/test_stats_account_data.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from auditui.stats.account import (
|
||||
get_account_info,
|
||||
get_country,
|
||||
get_subscription_details,
|
||||
)
|
||||
|
||||
|
||||
class AccountClient:
|
||||
"""Minimal API client returning endpoint-specific account responses."""
|
||||
|
||||
def __init__(self, responses: dict[str, dict]) -> None:
|
||||
"""Store endpoint response map for deterministic tests."""
|
||||
self._responses = responses
|
||||
|
||||
def get(self, path: str, **kwargs: object) -> dict:
|
||||
"""Return configured response and ignore query parameters."""
|
||||
del kwargs
|
||||
return self._responses.get(path, {})
|
||||
|
||||
|
||||
def test_get_account_info_merges_multiple_endpoints() -> None:
|
||||
"""Ensure account info aggregator combines endpoint payload dictionaries."""
|
||||
client = AccountClient(
|
||||
{
|
||||
"1.0/account/information": {"a": 1},
|
||||
"1.0/customer/information": {"b": 2},
|
||||
"1.0/customer/status": {"c": 3},
|
||||
}
|
||||
)
|
||||
assert get_account_info(client) == {"a": 1, "b": 2, "c": 3}
|
||||
|
||||
|
||||
def test_get_subscription_details_uses_known_nested_paths() -> None:
|
||||
"""Ensure first valid subscription_details list entry is returned."""
|
||||
info = {
|
||||
"customer_details": {
|
||||
"subscription": {"subscription_details": [{"name": "Plan"}]}
|
||||
}
|
||||
}
|
||||
assert get_subscription_details(info) == {"name": "Plan"}
|
||||
|
||||
|
||||
def test_get_country_supports_locale_variants() -> None:
|
||||
"""Ensure country extraction supports object, domain, and locale string forms."""
|
||||
auth_country_code = type(
|
||||
"Auth", (), {"locale": type("Loc", (), {"country_code": "us"})()}
|
||||
)()
|
||||
auth_domain = type("Auth", (), {"locale": type("Loc", (), {"domain": "fr"})()})()
|
||||
auth_string = type("Auth", (), {"locale": "en_gb"})()
|
||||
assert get_country(auth_country_code) == "US"
|
||||
assert get_country(auth_domain) == "FR"
|
||||
assert get_country(auth_string) == "GB"
|
||||
67
tests/stats/test_stats_aggregator_output.py
Normal file
67
tests/stats/test_stats_aggregator_output.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from auditui.stats.aggregator import StatsAggregator
|
||||
from auditui.stats import aggregator as aggregator_mod
|
||||
|
||||
|
||||
def test_get_stats_returns_empty_without_client() -> None:
|
||||
"""Ensure stats aggregation short-circuits when API client is absent."""
|
||||
aggregator = StatsAggregator(
|
||||
client=None, auth=None, library_client=None, all_items=[]
|
||||
)
|
||||
assert aggregator.get_stats() == []
|
||||
|
||||
|
||||
def test_get_stats_builds_expected_rows(monkeypatch) -> None:
|
||||
"""Ensure aggregator assembles rows from listening, account, and email sources."""
|
||||
monkeypatch.setattr(
|
||||
aggregator_mod.listening_mod, "get_signup_year", lambda _client: 2015
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
aggregator_mod.listening_mod,
|
||||
"get_listening_time",
|
||||
lambda _client, duration, start_date: 120_000 if duration == 1 else 3_600_000,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
aggregator_mod.listening_mod, "get_finished_books_count", lambda _lc, _items: 7
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
aggregator_mod.email_mod,
|
||||
"resolve_email",
|
||||
lambda *args, **kwargs: "user@example.com",
|
||||
)
|
||||
monkeypatch.setattr(aggregator_mod.account_mod, "get_country", lambda _auth: "US")
|
||||
monkeypatch.setattr(
|
||||
aggregator_mod.account_mod,
|
||||
"get_account_info",
|
||||
lambda _client: {
|
||||
"subscription_details": [
|
||||
{
|
||||
"name": "Premium",
|
||||
"next_bill_date": "2026-02-01T00:00:00Z",
|
||||
"next_bill_amount": {
|
||||
"currency_value": "14.95",
|
||||
"currency_code": "USD",
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
aggregator = StatsAggregator(
|
||||
client=object(),
|
||||
auth=object(),
|
||||
library_client=object(),
|
||||
all_items=[{}, {}, {}],
|
||||
)
|
||||
stats = dict(aggregator.get_stats(today=date(2026, 2, 1)))
|
||||
assert stats["Email"] == "user@example.com"
|
||||
assert stats["Country Store"] == "US"
|
||||
assert stats["Signup Year"] == "2015"
|
||||
assert stats["Subscription"] == "Premium"
|
||||
assert stats["Price"] == "14.95 USD"
|
||||
assert stats["This Month"] == "2m"
|
||||
assert stats["This Year"] == "1h00"
|
||||
assert stats["Books Finished"] == "7 / 3"
|
||||
64
tests/stats/test_stats_email_resolution.py
Normal file
64
tests/stats/test_stats_email_resolution.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from auditui.stats.email import (
|
||||
find_email_in_data,
|
||||
first_email,
|
||||
get_email_from_account_info,
|
||||
get_email_from_auth,
|
||||
get_email_from_auth_file,
|
||||
get_email_from_config,
|
||||
resolve_email,
|
||||
)
|
||||
|
||||
|
||||
def test_find_email_in_nested_data() -> None:
|
||||
"""Ensure nested structures are scanned until a plausible email is found."""
|
||||
data = {"a": {"b": ["nope", "user@example.com"]}}
|
||||
assert find_email_in_data(data) == "user@example.com"
|
||||
|
||||
|
||||
def test_first_email_skips_unknown_and_none() -> None:
|
||||
"""Ensure first_email ignores empty and Unknown sentinel values."""
|
||||
assert first_email(None, "Unknown", "ok@example.com") == "ok@example.com"
|
||||
|
||||
|
||||
def test_get_email_from_config_and_auth_file(tmp_path: Path) -> None:
|
||||
"""Ensure config and auth-file readers extract valid email fields."""
|
||||
config_path = tmp_path / "config.json"
|
||||
auth_path = tmp_path / "auth.json"
|
||||
config_path.write_text(
|
||||
json.dumps({"email": "config@example.com"}), encoding="utf-8"
|
||||
)
|
||||
auth_path.write_text(json.dumps({"email": "auth@example.com"}), encoding="utf-8")
|
||||
assert get_email_from_config(config_path) == "config@example.com"
|
||||
assert get_email_from_auth_file(auth_path) == "auth@example.com"
|
||||
|
||||
|
||||
def test_get_email_from_auth_prefers_username() -> None:
|
||||
"""Ensure auth object attributes are checked in expected precedence order."""
|
||||
auth = type(
|
||||
"Auth", (), {"username": "user@example.com", "login": None, "email": None}
|
||||
)()
|
||||
assert get_email_from_auth(auth) == "user@example.com"
|
||||
|
||||
|
||||
def test_get_email_from_account_info_supports_nested_customer_info() -> None:
|
||||
"""Ensure account email can be discovered in nested customer_info payload."""
|
||||
info = {"customer_info": {"primary_email": "nested@example.com"}}
|
||||
assert get_email_from_account_info(info) == "nested@example.com"
|
||||
|
||||
|
||||
def test_resolve_email_falls_back_to_account_getter(tmp_path: Path) -> None:
|
||||
"""Ensure resolve_email checks account-info callback when local sources miss."""
|
||||
auth = object()
|
||||
value = resolve_email(
|
||||
auth,
|
||||
client=object(),
|
||||
config_path=tmp_path / "missing-config.json",
|
||||
auth_path=tmp_path / "missing-auth.json",
|
||||
get_account_info=lambda: {"customer_email": "account@example.com"},
|
||||
)
|
||||
assert value == "account@example.com"
|
||||
16
tests/stats/test_stats_formatting.py
Normal file
16
tests/stats/test_stats_formatting.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from auditui.stats.format import format_date, format_time
|
||||
|
||||
|
||||
def test_format_time_handles_minutes_and_hours() -> None:
|
||||
"""Ensure format_time outputs minute-only and hour-minute formats."""
|
||||
assert format_time(90_000) == "1m"
|
||||
assert format_time(3_660_000) == "1h01"
|
||||
|
||||
|
||||
def test_format_date_handles_iso_and_invalid_values() -> None:
|
||||
"""Ensure format_date normalizes ISO timestamps and preserves invalid input."""
|
||||
assert format_date("2026-01-15T10:20:30Z") == "2026-01-15"
|
||||
assert format_date("not-a-date") == "not-a-date"
|
||||
assert format_date(None) == "Unknown"
|
||||
64
tests/stats/test_stats_listening_metrics.py
Normal file
64
tests/stats/test_stats_listening_metrics.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from auditui.stats.listening import (
|
||||
get_finished_books_count,
|
||||
get_listening_time,
|
||||
get_signup_year,
|
||||
has_activity,
|
||||
)
|
||||
|
||||
|
||||
class StatsClient:
|
||||
"""Client double for monthly aggregate lookups keyed by start date."""
|
||||
|
||||
def __init__(self, sums_by_start_date: dict[str, list[int]]) -> None:
|
||||
"""Store aggregate sums grouped by monthly_listening_interval_start_date."""
|
||||
self._sums = sums_by_start_date
|
||||
|
||||
def get(self, path: str, **kwargs: str) -> dict:
|
||||
"""Return aggregate payload based on requested interval start date."""
|
||||
del path
|
||||
start_date = kwargs["monthly_listening_interval_start_date"]
|
||||
sums = self._sums.get(start_date, [0])
|
||||
return {
|
||||
"aggregated_monthly_listening_stats": [{"aggregated_sum": s} for s in sums]
|
||||
}
|
||||
|
||||
|
||||
def test_has_activity_detects_non_zero_months() -> None:
|
||||
"""Ensure activity helper returns true when any month has positive sum."""
|
||||
assert (
|
||||
has_activity(
|
||||
{
|
||||
"aggregated_monthly_listening_stats": [
|
||||
{"aggregated_sum": 0},
|
||||
{"aggregated_sum": 1},
|
||||
]
|
||||
}
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_get_listening_time_sums_aggregated_months() -> None:
|
||||
"""Ensure monthly aggregate sums are added into one listening total."""
|
||||
client = StatsClient({"2026-01": [1000, 2000, 3000]})
|
||||
assert get_listening_time(client, duration=1, start_date="2026-01") == 6000
|
||||
|
||||
|
||||
def test_get_signup_year_returns_earliest_year_with_activity() -> None:
|
||||
"""Ensure signup year search finds first active year via binary search."""
|
||||
client = StatsClient(
|
||||
{"2026-01": [1], "2010-01": [1], "2002-01": [1], "2001-01": [0]}
|
||||
)
|
||||
year = get_signup_year(client)
|
||||
assert year <= 2010
|
||||
|
||||
|
||||
def test_get_finished_books_count_uses_library_is_finished() -> None:
|
||||
"""Ensure finished books count delegates to library client predicate."""
|
||||
library_client = type(
|
||||
"Library", (), {"is_finished": lambda self, item: item.get("done", False)}
|
||||
)()
|
||||
items = [{"done": True}, {"done": False}, {"done": True}]
|
||||
assert get_finished_books_count(library_client, items) == 2
|
||||
Reference in New Issue
Block a user