"""回归测试: bzzoiro 采集链路统计字段映射。 ⚠️ 重要说明: 当前字段名基于常见足球 API 模式推测,未经真实 bzzoiro 响应校验。 以下测试验证的是「若真实字段与推测一致,映射应正确」的假设。 待用户提供真实 event 样例后,需核对并修正以下字段名: - 射门: home_shots / away_shots - 射正: home_shots_on_target / away_shots_on_target - 角球: home_corners / away_corners - 控球: home_possession - xG: home_xg / away_xg - 黄牌: home_yellow_cards / away_yellow_cards - 红牌: home_red_cards / away_red_cards """ from __future__ import annotations from datetime import datetime, timezone import pytest from src.data.normalize import NormalizedMatch, normalize_bzzoiro class TestNormalizeBzzoirotStats: """normalize_bzzoiro 应正确映射统计字段(基于推测字段名)。""" def test_maps_shots(self): """API 提供 shots 字段时应正确映射。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "Man City", "away_team": "Man United", "home_score": 2, "away_score": 1, "home_shots": 15, "away_shots": 8, "home_shots_on_target": 6, "away_shots_on_target": 3, } m = normalize_bzzoiro(raw, "E0") assert m is not None assert m.home_shots == 15 assert m.away_shots == 8 assert m.home_shots_on_target == 6 assert m.away_shots_on_target == 3 def test_maps_corners(self): """API 提供 corners 字段时应正确映射。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "Liverpool", "away_team": "Chelsea", "home_score": 1, "away_score": 1, "home_corners": 7, "away_corners": 4, } m = normalize_bzzoiro(raw, "E0") assert m is not None assert m.home_corners == 7 assert m.away_corners == 4 def test_maps_possession(self): """API 提供 possession 字段时应正确映射。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "Barcelona", "away_team": "Real Madrid", "home_score": 2, "away_score": 0, "home_possession": 62.5, } m = normalize_bzzoiro(raw, "SP1") assert m is not None assert m.home_possession == 62.5 def test_maps_xg(self): """API 提供 xG 字段时应正确映射。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "Bayern", "away_team": "Dortmund", "home_score": 3, "away_score": 1, "home_xg": 2.5, "away_xg": 0.8, } m = normalize_bzzoiro(raw, "D1") assert m is not None assert m.home_xg == 2.5 assert m.away_xg == 0.8 def test_maps_cards(self): """API 提供 cards 字段时应正确映射。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "Arsenal", "away_team": "Tottenham", "home_score": 1, "away_score": 0, "home_yellow_cards": 2, "away_yellow_cards": 3, "home_red_cards": 0, "away_red_cards": 1, } m = normalize_bzzoiro(raw, "E0") assert m is not None assert m.home_yellow_cards == 2 assert m.away_yellow_cards == 3 assert m.home_red_cards == 0 assert m.away_red_cards == 1 def test_missing_stats_stay_none(self): """API 没有统计字段时应保持 None,不伪造。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "A", "away_team": "B", "home_score": 1, "away_score": 0, } m = normalize_bzzoiro(raw, "E0") assert m is not None # 无比分数据时不应有统计字段 assert m.home_shots is None assert m.away_shots is None assert m.home_xg is None assert m.away_xg is None assert m.home_possession is None assert m.home_corners is None def test_alternative_field_names(self): """API 使用替代字段名时也应正确映射。""" raw = { "event_date": "2026-01-15T15:00:00Z", "status": "finished", "home_team": "A", "away_team": "B", "home_score": 1, "away_score": 0, "shots_home": 12, "shots_away": 6, "sot_home": 5, "sot_away": 2, "corners_home": 8, "corners_away": 3, "xg_home": 1.8, "xg_away": 0.5, } m = normalize_bzzoiro(raw, "E0") assert m is not None assert m.home_shots == 12 assert m.away_shots == 6 assert m.home_shots_on_target == 5 assert m.away_shots_on_target == 2 assert m.home_corners == 8 assert m.away_corners == 3 assert m.home_xg == 1.8 assert m.away_xg == 0.5 class TestIngestionCondition: """入库条件应不再强制要求 xG。""" def test_any_stat_field_triggers_stats_creation(self): """任一统计字段存在即可触发 MatchStats 创建。""" # 模拟 normalize 后的结果 nm = NormalizedMatch( league_type="E0", date=datetime(2026, 1, 15, 15, 0, tzinfo=timezone.utc), home_team="A", away_team="B", match_status="finished", home_goals=1, away_goals=0, # 只有 shots,无 xG home_shots=10, away_shots=5, ) # 验证:任一统计字段存在 has_stats = ( nm.home_xg is not None or nm.away_xg is not None or nm.home_shots is not None or nm.away_shots is not None or nm.home_corners is not None or nm.away_corners is not None or nm.home_possession is not None ) assert has_stats is True, "有 shots 时应视为有统计数据" def test_no_stats_means_no_match_stats(self): """没有任何统计字段时不应创建 MatchStats。""" nm = NormalizedMatch( league_type="E0", date=datetime(2026, 1, 15, 15, 0, tzinfo=timezone.utc), home_team="A", away_team="B", match_status="finished", home_goals=1, away_goals=0, ) has_stats = ( nm.home_xg is not None or nm.away_xg is not None or nm.home_shots is not None or nm.away_shots is not None or nm.home_corners is not None or nm.away_corners is not None or nm.home_possession is not None ) assert has_stats is False, "无统计字段时不应创建 MatchStats"