mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-06 18:59:37 +00:00
- Added configuration options for announcing red cards, yellow cards, and match stoppages in `config.ini.example`. - Updated `ESPNClient` to parse and include red and yellow card details in match state data. - Enhanced `WorldCupLiveService` to announce red and yellow cards, including formatted messages for players and stoppage events. - Implemented unit tests to validate the new card and stoppage announcement functionalities, ensuring accurate reporting during matches.
415 lines
20 KiB
Python
415 lines
20 KiB
Python
"""Tests for the World Cup live-score service (detection + tick flow)."""
|
|
|
|
import configparser
|
|
from unittest.mock import AsyncMock, MagicMock, Mock
|
|
|
|
from modules.service_plugins.worldcup_service import WorldCupLiveService
|
|
|
|
|
|
def _make_bot(**overrides):
|
|
bot = MagicMock()
|
|
bot.logger = Mock()
|
|
config = configparser.ConfigParser()
|
|
config.add_section("Worldcup_Service")
|
|
config.set("Worldcup_Service", "enabled", "true")
|
|
config.set("Worldcup_Service", "channel", "#fifa")
|
|
for k, v in overrides.items():
|
|
config.set("Worldcup_Service", k, v)
|
|
bot.config = config
|
|
# In-memory metadata store
|
|
store = {}
|
|
bot.db_manager = MagicMock()
|
|
bot.db_manager.get_metadata = Mock(side_effect=lambda k: store.get(k))
|
|
bot.db_manager.set_metadata = Mock(side_effect=lambda k, v: store.update({k: v}))
|
|
bot.command_manager = MagicMock()
|
|
bot.command_manager.send_channel_message = AsyncMock(return_value=True)
|
|
return bot
|
|
|
|
|
|
def _svc(**overrides):
|
|
svc = WorldCupLiveService(_make_bot(**overrides))
|
|
# Avoid external-notification config lookups in _post
|
|
svc.has_external_notification_targets = lambda: False
|
|
svc.get_mesh_flood_scope = lambda: None
|
|
return svc
|
|
|
|
|
|
def _match(eid="1", h=0, a=0, status="STATUS_SCHEDULED", clock="", hp=None, ap=None,
|
|
hid="100", aid="200", hn="Côte d'Ivoire", an="Ecuador", goals=None, cards=None, yellows=None):
|
|
return {
|
|
"id": eid, "home_id": hid, "away_id": aid, "home_name": hn, "away_name": an,
|
|
"home_score": h, "away_score": a, "status": status, "clock": clock,
|
|
"home_pen": hp, "away_pen": ap, "goals": goals or [], "cards": cards or [], "yellows": yellows or [],
|
|
}
|
|
|
|
|
|
def _goal(clock, scorer, own_goal=False, penalty=False, team_id="100", kind=""):
|
|
return {"clock": clock, "scorer": scorer, "team_id": team_id, "own_goal": own_goal,
|
|
"penalty": penalty, "kind": kind}
|
|
|
|
|
|
def _card(clock, player, team_id="200"):
|
|
return {"clock": clock, "player": player, "team_id": team_id}
|
|
|
|
|
|
GROUPS = {"100": "Group E", "200": "Group E"}
|
|
|
|
|
|
class TestDetect:
|
|
def _detect(self, svc, prev, m, in_group=True):
|
|
committed = min(len(m.get("goals") or []), m["home_score"] + m["away_score"])
|
|
cur = {"h": m["home_score"], "a": m["away_score"], "s": m["status"], "g": committed,
|
|
"lg": prev.get("lg")}
|
|
return svc._detect(prev, cur, m, in_group, GROUPS, "Round of 32")
|
|
|
|
def test_kickoff(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_SCHEDULED"}
|
|
m = _match(status="STATUS_FIRST_HALF")
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire vs Ecuador (kick-off)"
|
|
|
|
def test_goal_detail_before_score_not_announced(self):
|
|
# A scoring play can appear before the score updates (or while under VAR review).
|
|
# The score is still 0-0, so do not announce a phantom goal.
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0}
|
|
m = _match(h=0, a=0, status="STATUS_FIRST_HALF", clock="17'", goals=[_goal("17'", "Lionel Messi")])
|
|
assert self._detect(svc, prev, m) is None
|
|
|
|
def test_goal_old_state_uses_score_baseline(self):
|
|
# State saved before the scorer feature has no "g"; the prior score total is the
|
|
# baseline, so a real new goal is announced (named), not replayed or dropped.
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF"}
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF", clock="23'", goals=[_goal("23'", "Erling Haaland")])
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 1, Ecuador 0 (23' Erling Haaland)"
|
|
|
|
def test_goal_names_new_scorer(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0}
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF", clock="7'",
|
|
goals=[_goal("7'", "Elijah Just")])
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 1, Ecuador 0 (7' Elijah Just)"
|
|
|
|
def test_goal_header_flavor(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0}
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF", goals=[_goal("30'", "Cody Gakpo", kind="header")])
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 1, Ecuador 0 (30' Cody Gakpo, header)"
|
|
|
|
def test_goal_penalty_annotation(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_SECOND_HALF", "g": 1}
|
|
m = _match(h=1, a=1, status="STATUS_SECOND_HALF", clock="64'",
|
|
goals=[_goal("7'", "Elijah Just"), _goal("64'", "Ramin Rezaeian", penalty=True, team_id="200")])
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 1, Ecuador 1 (64' Ramin Rezaeian, pen)"
|
|
|
|
def test_goal_multiple_since_last_poll(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0}
|
|
m = _match(h=2, a=0, status="STATUS_FIRST_HALF", clock="20'",
|
|
goals=[_goal("7'", "A. One"), _goal("20'", "B. Two")])
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 2, Ecuador 0 (7' A. One; 20' B. Two)"
|
|
|
|
def test_goal_without_details_not_announced(self):
|
|
# Score moved but no scoring-play detail yet (ESPN lag): wait for the named detail
|
|
# rather than posting an unnamed update now (it would double-post when it arrives).
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0}
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF", clock="12'") # no goals payload yet
|
|
assert self._detect(svc, prev, m) is None
|
|
|
|
def test_goal_announced_when_detail_arrives_after_score(self):
|
|
# The poll after the score ticked, the named scoring play appears -> announce it,
|
|
# showing the (already-updated) score.
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0} # score already 1-0, no detail yet
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF", clock="13'", goals=[_goal("12'", "Erling Haaland")])
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 1, Ecuador 0 (12' Erling Haaland)"
|
|
|
|
def test_disallowed_goal_names_scorer(self):
|
|
# A previously-announced goal is rescinded (score reverts, scoring play removed).
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_FIRST_HALF", "g": 1, "lg": "60' Lionel Messi"}
|
|
m = _match(h=0, a=0, status="STATUS_FIRST_HALF", goals=[])
|
|
assert self._detect(svc, prev, m) == \
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 — 60' Lionel Messi ruled out (VAR)"
|
|
|
|
def test_disallowed_goal_generic_when_scorer_unknown(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_FIRST_HALF", "g": 1} # no remembered scorer
|
|
m = _match(h=0, a=0, status="STATUS_FIRST_HALF", goals=[])
|
|
assert self._detect(svc, prev, m) == \
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 — goal ruled out (VAR)"
|
|
|
|
def test_disallowed_can_be_toggled_off(self):
|
|
svc = _svc(announce_disallowed="false")
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_FIRST_HALF", "g": 1, "lg": "60' Lionel Messi"}
|
|
m = _match(h=0, a=0, status="STATUS_FIRST_HALF", goals=[])
|
|
assert self._detect(svc, prev, m) is None
|
|
|
|
def test_phantom_play_removed_is_not_a_disallowed(self):
|
|
# A play that was never announced (committed stayed 0) being removed must NOT
|
|
# produce a "ruled out" message.
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "g": 0}
|
|
m = _match(h=0, a=0, status="STATUS_FIRST_HALF", goals=[])
|
|
assert self._detect(svc, prev, m) is None
|
|
|
|
def test_halftime(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF"}
|
|
m = _match(h=0, a=0, status="STATUS_HALFTIME")
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 0, Ecuador 0 (half-time)"
|
|
|
|
def test_fulltime(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 1, "s": "STATUS_SECOND_HALF"}
|
|
m = _match(h=1, a=2, status="STATUS_FULL_TIME")
|
|
assert self._detect(svc, prev, m) == "Group E: Côte d'Ivoire 1, Ecuador 2 (full-time)"
|
|
|
|
def test_fulltime_penalties(self):
|
|
svc = _svc()
|
|
prev = {"h": 2, "a": 2, "s": "STATUS_SECOND_HALF"}
|
|
m = _match(h=2, a=2, status="STATUS_FINAL_PEN", hp=3, ap=4)
|
|
assert self._detect(svc, prev, m, in_group=False) == \
|
|
"Round of 32: Côte d'Ivoire 2, Ecuador 2 (full-time, pens 3-4)"
|
|
|
|
def test_no_change_returns_none(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_FIRST_HALF"}
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF")
|
|
assert self._detect(svc, prev, m) is None
|
|
|
|
def test_triggers_can_be_disabled(self):
|
|
svc = _svc(announce_goals="false")
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF"}
|
|
m = _match(h=1, a=0, status="STATUS_FIRST_HALF", clock="23'")
|
|
assert self._detect(svc, prev, m) is None
|
|
|
|
def test_knockout_uses_stage_label(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_SCHEDULED"}
|
|
m = _match(status="STATUS_FIRST_HALF")
|
|
# in_group False -> stage label instead of group
|
|
cur = {"h": 0, "a": 0, "s": m["status"]}
|
|
msg = svc._detect(prev, cur, m, False, GROUPS, "Round of 32")
|
|
assert msg.startswith("Round of 32: ")
|
|
|
|
|
|
class TestRedCards:
|
|
def _red(self, svc, prev, m, in_group=True):
|
|
cur = {
|
|
"h": m["home_score"], "a": m["away_score"], "s": m["status"],
|
|
"g": min(len(m.get("goals") or []), m["home_score"] + m["away_score"]),
|
|
"rc": len(m.get("cards") or []), "yc": len(m.get("yellows") or []), "lg": prev.get("lg"),
|
|
}
|
|
return svc._detect_card(prev, cur, m, in_group, GROUPS, "Round of 32",
|
|
count_key="rc", list_key="cards", word="red card")
|
|
|
|
def test_red_card_names_player_and_team(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_SECOND_HALF", "rc": 0}
|
|
m = _match(h=0, a=0, status="STATUS_SECOND_HALF", cards=[_card("80'", "Tarik Muharemovic", team_id="200")])
|
|
assert self._red(svc, prev, m) == \
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 — red card: Tarik Muharemovic (Ecuador, 80')"
|
|
|
|
def test_red_card_home_team_resolved(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_SECOND_HALF", "rc": 0}
|
|
m = _match(h=1, a=0, status="STATUS_SECOND_HALF", cards=[_card("55'", "A Defender", team_id="100")])
|
|
assert self._red(svc, prev, m) == \
|
|
"Group E: Côte d'Ivoire 1, Ecuador 0 — red card: A Defender (Côte d'Ivoire, 55')"
|
|
|
|
def test_red_card_only_new_one_announced(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_SECOND_HALF", "rc": 1}
|
|
m = _match(h=0, a=0, status="STATUS_SECOND_HALF",
|
|
cards=[_card("40'", "First"), _card("80'", "Second", team_id="200")])
|
|
assert self._red(svc, prev, m) == \
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 — red card: Second (Ecuador, 80')"
|
|
|
|
def test_red_card_old_state_rebaselines(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_SECOND_HALF"} # no "rc"
|
|
m = _match(h=0, a=0, status="STATUS_SECOND_HALF", cards=[_card("80'", "Tarik Muharemovic")])
|
|
assert self._red(svc, prev, m) is None
|
|
|
|
def test_red_card_not_announced_when_not_in_play(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_SECOND_HALF", "rc": 0}
|
|
m = _match(h=1, a=0, status="STATUS_FULL_TIME", cards=[_card("80'", "Late Red")])
|
|
assert self._red(svc, prev, m) is None
|
|
|
|
|
|
class TestYellowCardsAndStoppage:
|
|
def _yellow(self, svc, prev, m, in_group=True):
|
|
cur = {
|
|
"h": m["home_score"], "a": m["away_score"], "s": m["status"],
|
|
"g": min(len(m.get("goals") or []), m["home_score"] + m["away_score"]),
|
|
"rc": len(m.get("cards") or []), "yc": len(m.get("yellows") or []), "lg": prev.get("lg"),
|
|
}
|
|
return svc._detect_card(prev, cur, m, in_group, GROUPS, "Round of 32",
|
|
count_key="yc", list_key="yellows", word="yellow card")
|
|
|
|
def test_yellow_card_formatted(self):
|
|
svc = _svc(announce_yellow_cards="true")
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_FIRST_HALF", "yc": 0}
|
|
m = _match(h=0, a=0, status="STATUS_FIRST_HALF", yellows=[_card("33'", "Teboho Mokoena", team_id="100")])
|
|
assert self._yellow(svc, prev, m) == \
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 — yellow card: Teboho Mokoena (Côte d'Ivoire, 33')"
|
|
|
|
async def test_yellow_off_by_default_through_tick(self):
|
|
svc = _svc() # announce_yellow_cards defaults False
|
|
assert svc.announce_yellow_cards is False
|
|
svc.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=0, a=0, status="STATUS_FIRST_HALF")])
|
|
await svc._tick() # seed
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[
|
|
_match(h=0, a=0, status="STATUS_FIRST_HALF", yellows=[_card("33'", "Booked")])
|
|
])
|
|
await svc._tick()
|
|
svc.bot.command_manager.send_channel_message.assert_not_awaited()
|
|
|
|
def test_stoppage_abandoned(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_SECOND_HALF", "g": 1}
|
|
m = _match(h=1, a=0, status="STATUS_ABANDONED")
|
|
cur = {"h": 1, "a": 0, "s": "STATUS_ABANDONED", "g": 1, "rc": 0, "yc": 0, "lg": None}
|
|
assert svc._detect(prev, cur, m, True, GROUPS, "Round of 32") == \
|
|
"Group E: Côte d'Ivoire 1, Ecuador 0 (abandoned)"
|
|
|
|
def test_stoppage_postponed_from_scheduled(self):
|
|
svc = _svc()
|
|
prev = {"h": 0, "a": 0, "s": "STATUS_SCHEDULED", "g": 0}
|
|
m = _match(h=0, a=0, status="STATUS_POSTPONED")
|
|
cur = {"h": 0, "a": 0, "s": "STATUS_POSTPONED", "g": 0, "rc": 0, "yc": 0, "lg": None}
|
|
assert svc._detect(prev, cur, m, True, GROUPS, "Round of 32") == \
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 (postponed)"
|
|
|
|
def test_stoppage_not_repeated(self):
|
|
svc = _svc()
|
|
prev = {"h": 1, "a": 0, "s": "STATUS_SUSPENDED", "g": 1}
|
|
m = _match(h=1, a=0, status="STATUS_SUSPENDED")
|
|
cur = {"h": 1, "a": 0, "s": "STATUS_SUSPENDED", "g": 1, "rc": 0, "yc": 0, "lg": None}
|
|
assert svc._detect(prev, cur, m, True, GROUPS, "Round of 32") is None
|
|
|
|
|
|
class TestTick:
|
|
async def test_seeds_silently_then_announces(self):
|
|
svc = _svc()
|
|
svc.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
|
|
# First poll: match already in progress -> seeded, no message
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=0, a=0, status="STATUS_FIRST_HALF")])
|
|
await svc._tick()
|
|
svc.bot.command_manager.send_channel_message.assert_not_awaited()
|
|
|
|
# Second poll: a goal (with scoring-play detail) -> announced with scorer
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[
|
|
_match(h=1, a=0, status="STATUS_FIRST_HALF", clock="12'", goals=[_goal("12'", "Erling Haaland")])
|
|
])
|
|
await svc._tick()
|
|
svc.bot.command_manager.send_channel_message.assert_awaited_once()
|
|
args = svc.bot.command_manager.send_channel_message.await_args.args
|
|
assert args[0] == "#fifa"
|
|
assert args[1] == "Group E: Côte d'Ivoire 1, Ecuador 0 (12' Erling Haaland)"
|
|
|
|
async def test_polls_faster_while_live(self):
|
|
svc = _svc(live_poll_interval="20000", poll_interval="60000")
|
|
svc.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
|
|
# A live match -> fast cadence
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(status="STATUS_FIRST_HALF")])
|
|
assert await svc._tick() == 20.0
|
|
|
|
# Only scheduled/finished matches -> normal cadence
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(status="STATUS_SCHEDULED")])
|
|
assert await svc._tick() == 60.0
|
|
|
|
async def test_goal_then_var_disallowed_lifecycle(self):
|
|
svc = _svc()
|
|
svc.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
|
|
# seed at 0-0
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=0, a=0, status="STATUS_FIRST_HALF")])
|
|
await svc._tick()
|
|
# goal -> announced, scorer remembered
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[
|
|
_match(h=1, a=0, status="STATUS_FIRST_HALF", goals=[_goal("60'", "Lionel Messi")])
|
|
])
|
|
await svc._tick()
|
|
# VAR reverses it -> separate "ruled out" notification
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=0, a=0, status="STATUS_FIRST_HALF", goals=[])])
|
|
await svc._tick()
|
|
|
|
posted = [c.args[1] for c in svc.bot.command_manager.send_channel_message.await_args_list]
|
|
assert posted == [
|
|
"Group E: Côte d'Ivoire 1, Ecuador 0 (60' Lionel Messi)",
|
|
"Group E: Côte d'Ivoire 0, Ecuador 0 — 60' Lionel Messi ruled out (VAR)",
|
|
]
|
|
|
|
async def test_goal_and_red_card_same_poll_both_announced(self):
|
|
svc = _svc()
|
|
svc.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=0, a=0, status="STATUS_SECOND_HALF")])
|
|
await svc._tick() # seed
|
|
svc.espn_client.fetch_match_states = AsyncMock(return_value=[
|
|
_match(h=1, a=0, status="STATUS_SECOND_HALF",
|
|
goals=[_goal("70'", "Scorer")], cards=[_card("70'", "Sent Off", team_id="200")])
|
|
])
|
|
await svc._tick()
|
|
posted = [c.args[1] for c in svc.bot.command_manager.send_channel_message.await_args_list]
|
|
assert posted == [
|
|
"Group E: Côte d'Ivoire 1, Ecuador 0 (70' Scorer)",
|
|
"Group E: Côte d'Ivoire 1, Ecuador 0 — red card: Sent Off (Ecuador, 70')",
|
|
]
|
|
|
|
async def test_idle_when_no_tournament(self):
|
|
svc = _svc()
|
|
svc.wc_data.get_active_tournament = AsyncMock(return_value=None)
|
|
svc.espn_client.fetch_match_states = AsyncMock()
|
|
delay = await svc._tick()
|
|
assert delay == svc.idle_interval_seconds
|
|
svc.espn_client.fetch_match_states.assert_not_awaited()
|
|
|
|
async def test_state_persisted_across_restart(self):
|
|
bot = _make_bot()
|
|
svc1 = WorldCupLiveService(bot)
|
|
svc1.has_external_notification_targets = lambda: False
|
|
svc1.get_mesh_flood_scope = lambda: None
|
|
svc1.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc1.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
svc1.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=1, a=0, status="STATUS_FIRST_HALF")])
|
|
await svc1._tick() # seeds 1-0
|
|
|
|
# New instance reuses the same bot/db -> loads persisted state, no re-announce of seed
|
|
svc2 = WorldCupLiveService(bot)
|
|
svc2.has_external_notification_targets = lambda: False
|
|
svc2.get_mesh_flood_scope = lambda: None
|
|
svc2.wc_data.get_active_tournament = AsyncMock(
|
|
return_value={"league": "fifa.world", "in_group_stage": True, "stage_label": "Group"}
|
|
)
|
|
svc2.wc_data.get_team_groups = AsyncMock(return_value=GROUPS)
|
|
svc2.espn_client.fetch_match_states = AsyncMock(return_value=[_match(h=1, a=0, status="STATUS_FIRST_HALF")])
|
|
await svc2._tick()
|
|
svc2.bot.command_manager.send_channel_message.assert_not_awaited()
|
|
assert svc2._state["1"]["h"] == 1
|