"""Basic unit tests for TeamExtractor and Elo analyzers. This module uses a simple in-memory FakeDB to validate: - Team aliases union behavior - Elo snapshot and history persistence contracts """ import unittest from typing import Any, Dict, List from databank.analytics.teams import TeamExtractorAnalyzer from databank.analytics.elo import EloAnalyzer from databank.core.models import Document class FakeDB: """A minimal in-memory DB stub implementing insert_many/find for tests.""" def __init__(self) -> None: self.storage: Dict[str, List[Dict[str, Any]]] = {} def connect(self) -> None: # for parity """No-op connect to match real DB interface.""" return None def insert_many(self, docs: List[Document]) -> int: """Append documents by kind; return inserted count.""" count = 0 for d in docs: kind = d.kind rec = {"_id": d.id, "kind": kind, "data": d.data} self.storage.setdefault(kind, []).append(rec) count += 1 return count def find( self, kind: str, query: Dict[str, Any] | None = None, _projection=None, _limit=None, ): # noqa: A002 - filter naming for parity """Retrieve documents by kind; supports _id $in filter.""" data = self.storage.get(kind, []) if not query: return list(data) # Support _id $in ids = None if "_id" in query and isinstance(query["_id"], dict) and "$in" in query["_id"]: ids = set(map(str, query["_id"]["$in"])) if ids is not None: return [d for d in data if str(d.get("_id")) in ids] return list(data) class TestAnalyticsTeamElo(unittest.TestCase): """Tests for TeamExtractorAnalyzer and EloAnalyzer integrations.""" def test_team_extractor_alias_union(self) -> None: """Teams analyzer should union new aliases with existing ones.""" db = FakeDB() # Existing team with alias db.insert_many( [ Document( id="100", kind="teams", data={ "team_id": "100", "name_canonical": "manchester city", "aliases": ["曼城"], }, ) ] ) # Two matches with different alias spellings matches = [ { "match": { "homeTeamId": 100, "homeTeamName": "Manchester City", "awayTeamId": 200, "awayTeamName": "Arsenal", } }, { "match": { "homeTeamId": 300, "homeTeamName": "Chelsea", "awayTeamId": 100, "awayTeamName": "Man City", } }, ] res = TeamExtractorAnalyzer().compute(matches, db=db) self.assertIn("upserted", res) # Check merged aliases contain existing + new teams = db.find("teams") # Find all records for _id 100 and pick the latest appended (upsert effect) mlist = [x for x in teams if str(x.get("_id")) == "100"] self.assertGreaterEqual(len(mlist), 1) mrec = mlist[-1] aliases = set(mrec["data"]["aliases"]) # type: ignore[index] self.assertTrue({"曼城", "Manchester City", "Man City"}.issubset(aliases)) def test_elo_persist_snapshot_and_history(self) -> None: """Elo analyzer should persist snapshot and two history entries per match.""" db = FakeDB() # Two matches: A beats B 1-0, then draw 0-0 matches = [ { "match": { "matchId": 1, "homeTeamId": "A", "awayTeamId": "B", "homeScore": 1, "awayScore": 0, "elapsedTime": "已完场", "matchTime": 1_700_000_000, } }, { "match": { "matchId": 2, "homeTeamId": "A", "awayTeamId": "B", "homeScore": 0, "awayScore": 0, "elapsedTime": "已完场", "matchTime": 1_700_000_100, } }, ] res = EloAnalyzer().compute(matches, db=db, persist=True) self.assertEqual(res["processed"], 2) # Snapshot should exist elo = db.find("elo_ratings") self.assertTrue(len(elo) >= 2) # History should have 4 entries (2 matches x 2 teams) hist = db.find("ratings_history") self.assertEqual(len(hist), 4) # Ensure history contains match_id and ts for h in hist: self.assertIn("match_id", h["data"]) # type: ignore[index] self.assertIn("ts", h["data"]) # type: ignore[index] if __name__ == "__main__": unittest.main()