forked from sass/tipibot
Merge branch 'master' of ssh://git.lapikud.ee:202/renkar/tipibot
# Conflicts: # bot.py
This commit is contained in:
101
tests/test_fienta.py
Normal file
101
tests/test_fienta.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Tests for the Fienta ticket parsing that feeds team-role sync.
|
||||
|
||||
Covers the risky bit: turning raw Fienta ticket JSON into a reliable
|
||||
{discord identity -> team} + {team -> game} mapping, including which ticket
|
||||
types count as team members and how the game is detected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import config # noqa: E402
|
||||
from core import fienta # noqa: E402
|
||||
|
||||
|
||||
def _ticket(ttype: str, *, team="", discord="", discord_id="", nick=""):
|
||||
return {
|
||||
"order_id": 1,
|
||||
"rows": [{
|
||||
"ticket_type": {"title": ttype},
|
||||
"attendee": {
|
||||
fienta._F_TEAM_NAME: team,
|
||||
fienta._F_DISCORD_USERNAME: discord,
|
||||
fienta._F_DISCORD_USERID: discord_id,
|
||||
fienta._F_DISCORD_USERNAME.replace("discord", "x"): "",
|
||||
"nickname_134815": nick,
|
||||
},
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
CS2 = "Counter-Strike 2 Tournament - competitor ticket"
|
||||
LOL = "League of Legends Tournament - competitor ticket"
|
||||
|
||||
|
||||
def test_parse_maps_username_and_userid_to_team():
|
||||
fienta.parse_tickets([
|
||||
_ticket(CS2, team="KONE", discord="ar7enchik", discord_id="123"),
|
||||
])
|
||||
assert fienta.get_team_for_username("ar7enchik") == "KONE"
|
||||
assert fienta.get_team_for_username("AR7ENCHIK") == "KONE" # case-insensitive
|
||||
assert fienta.get_team_for_userid(123) == "KONE"
|
||||
assert fienta.get_team_game("KONE") == "CS2"
|
||||
|
||||
|
||||
def test_parse_detects_game_from_ticket_type():
|
||||
fienta.parse_tickets([
|
||||
_ticket(CS2, team="KONE", discord="a"),
|
||||
_ticket(LOL, team="Ööbik", discord="b"),
|
||||
])
|
||||
assert fienta.get_team_game("KONE") == "CS2"
|
||||
assert fienta.get_team_game("Ööbik") == "LoL"
|
||||
|
||||
|
||||
def test_parse_excludes_non_player_ticket_types():
|
||||
fienta.parse_tickets([
|
||||
_ticket("Visitor's Ticket", team="", discord=""),
|
||||
_ticket("Early Bird - visitor ticket", team="X", discord="ghost"),
|
||||
_ticket("Counter-Strike 2 Tournament Waiting List", team="WL", discord="waiter"),
|
||||
_ticket("LAN area - Access Ticket", team="", discord=""),
|
||||
_ticket(CS2, team="KONE", discord="real"),
|
||||
])
|
||||
assert fienta.all_team_names() == {"KONE"}
|
||||
assert fienta.get_team_for_username("ghost") is None
|
||||
assert fienta.get_team_for_username("waiter") is None
|
||||
assert fienta.get_team_for_username("real") == "KONE"
|
||||
|
||||
|
||||
def test_parse_includes_coach_and_substitute():
|
||||
fienta.parse_tickets([
|
||||
_ticket("Counter-Strike 2 Coach/Manager - competitor ticket", team="KONE", discord="coach"),
|
||||
_ticket("CS2 Substitute Player - competitor ticket", team="KONE", discord="sub"),
|
||||
])
|
||||
assert fienta.get_team_for_username("coach") == "KONE"
|
||||
assert fienta.get_team_for_username("sub") == "KONE"
|
||||
|
||||
|
||||
def test_parse_skips_tickets_without_team_or_discord():
|
||||
fienta.parse_tickets([
|
||||
_ticket(CS2, team="", discord="noteam"), # no team -> skipped
|
||||
_ticket(CS2, team="KONE", discord=""), # team but no discord -> team known, no user
|
||||
])
|
||||
assert fienta.get_team_for_username("noteam") is None
|
||||
assert "KONE" in fienta.all_team_names()
|
||||
|
||||
|
||||
def test_get_team_dividers_maps_via_game(monkeypatch):
|
||||
monkeypatch.setattr(config, "TEAM_DIVIDERS", {"cs2_2026": 100, "lol_2026": 200})
|
||||
fienta.parse_tickets([
|
||||
_ticket(CS2, team="KONE", discord="a"),
|
||||
_ticket(LOL, team="Ööbik", discord="b"),
|
||||
])
|
||||
assert fienta.get_team_dividers() == {"KONE": 100, "Ööbik": 200}
|
||||
|
||||
|
||||
def test_strips_leading_at_from_discord_username():
|
||||
fienta.parse_tickets([_ticket(CS2, team="KONE", discord="@handle")])
|
||||
assert fienta.get_team_for_username("handle") == "KONE"
|
||||
383
tests/test_team_sync.py
Normal file
383
tests/test_team_sync.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""Tests for team-role sync from the tournament registration sheet.
|
||||
|
||||
Covers the risky parsing (turning a messy, multi-section, merged-cell sheet
|
||||
into {team: [discord usernames]}) and the roster-independent add/remove/
|
||||
auto-create behaviour of sync_team_role, using lightweight fakes for discord
|
||||
+ the sheets cache.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import config # noqa: E402
|
||||
from core import member_sync, sheets # noqa: E402
|
||||
from tests.conftest import run # noqa: E402
|
||||
|
||||
|
||||
# --- parse_lineup ----------------------------------------------------------
|
||||
|
||||
def test_parse_lineup_strips_citizenship_and_splits():
|
||||
cell = "TFT (EST), nqmm (EST), sn1rk (EST), Kevka (EST), Kalatexx (EST)"
|
||||
assert sheets.parse_lineup(cell) == ["TFT", "nqmm", "sn1rk", "Kevka", "Kalatexx"]
|
||||
|
||||
|
||||
def test_parse_lineup_keeps_names_containing_commas_and_semicolons():
|
||||
# A single player's descriptive name contains a ';' - must stay one name.
|
||||
cell = "Onu Klaus ; vahepeal ka mõni teine tegelane (EST), Lurban (EST)"
|
||||
assert sheets.parse_lineup(cell) == [
|
||||
"Onu Klaus ; vahepeal ka mõni teine tegelane",
|
||||
"Lurban",
|
||||
]
|
||||
|
||||
|
||||
def test_parse_lineup_handles_odd_usernames():
|
||||
cell = "-acc +vac (EST), m (EST), M1X3RRRRRR (EST)"
|
||||
assert sheets.parse_lineup(cell) == ["-acc +vac", "m", "M1X3RRRRRR"]
|
||||
|
||||
|
||||
def test_parse_lineup_mixed_citizenship():
|
||||
cell = "imp (LAT), milteg (EST), Freesies (EST)"
|
||||
assert sheets.parse_lineup(cell) == ["imp", "milteg", "Freesies"]
|
||||
|
||||
|
||||
def test_parse_lineup_empty():
|
||||
assert sheets.parse_lineup("") == []
|
||||
assert sheets.parse_lineup(" ") == []
|
||||
|
||||
|
||||
def test_parse_lineup_fallback_without_citizenship():
|
||||
assert sheets.parse_lineup("alice, bob") == ["alice", "bob"]
|
||||
|
||||
|
||||
# --- parse_team_rosters (multi-section sheet) ------------------------------
|
||||
|
||||
# Mirrors the real sheet: merged title rows, a header row, team rows, a blank
|
||||
# separator, then a SECOND section with a different column count.
|
||||
SHEET_ROWS = [
|
||||
["", "", "", "", "", "", ""],
|
||||
["name", "members", "vrs ranking", "registration_date", "game", "", ""],
|
||||
["[merged] TipiLAN 2026 CS2 Registration Log"] + [""] * 6,
|
||||
["No", "Team Name", "Lineup (nickname, citizenship)", "VRS Ranking",
|
||||
"Registration Timestamp", "Status", "Participation confirmed?"],
|
||||
["1", "Piirivalvurid", "TFT (EST), nqmm (EST)", "N/A", "01.05.2026 15:07", "Confirmed", "Yes"],
|
||||
["2", "GENESIS", "kapa (EST), neaQ (EST)", "N/A", "01.05.2026 15:24", "Confirmed", "Yes"],
|
||||
["", "", "", "", "", "", ""],
|
||||
["[merged] TipiLAN 2026 LoL Registration Log"] + [""] * 4,
|
||||
["No", "Team Name", "Lineup (nickname, citizenship)",
|
||||
"Registration Timestamp (dd.mm.yyyy hh:mm)", "Confirmation Status"],
|
||||
["1", "Pushing 30s", "Onu Klaus (EST), Lurban (EST)", "01.05.2026 21:40", ""],
|
||||
["", "", "", "", ""],
|
||||
]
|
||||
|
||||
|
||||
def test_parse_team_rosters_multiple_sections():
|
||||
rosters = sheets.parse_team_rosters(SHEET_ROWS)
|
||||
assert rosters == {
|
||||
"Piirivalvurid": ["TFT", "nqmm"],
|
||||
"GENESIS": ["kapa", "neaQ"],
|
||||
"Pushing 30s": ["Onu Klaus", "Lurban"],
|
||||
}
|
||||
|
||||
|
||||
def test_parse_team_rosters_ignores_non_table_content():
|
||||
# No header row anywhere -> nothing extracted, no crash.
|
||||
assert sheets.parse_team_rosters([["just", "some", "prose"], ["more"]]) == {}
|
||||
|
||||
|
||||
# --- parse_team_sections (which game/year a block of teams came from) ------
|
||||
|
||||
def test_parse_team_sections_tags_each_block_with_its_title():
|
||||
sections = sheets.parse_team_sections(SHEET_ROWS)
|
||||
assert [(s.title, sorted(s.rosters)) for s in sections] == [
|
||||
("[merged] TipiLAN 2026 CS2 Registration Log", ["GENESIS", "Piirivalvurid"]),
|
||||
("[merged] TipiLAN 2026 LoL Registration Log", ["Pushing 30s"]),
|
||||
]
|
||||
|
||||
|
||||
def test_parse_team_sections_title_does_not_leak_to_untitled_block():
|
||||
rows = [
|
||||
["[merged] TipiLAN 2026 CS2 Registration Log"] + [""] * 2,
|
||||
["No", "Team Name", "Lineup (nickname)"],
|
||||
["1", "GENESIS", "kapa (EST)"],
|
||||
["", "", ""],
|
||||
["No", "Team Name", "Lineup (nickname)"], # second block, no title above it
|
||||
["1", "Nameless", "someone (EST)"],
|
||||
]
|
||||
titles = [s.title for s in sheets.parse_team_sections(rows)]
|
||||
assert titles == ["[merged] TipiLAN 2026 CS2 Registration Log", ""]
|
||||
|
||||
|
||||
# --- resolve_divider (section title -> configured divider role) ------------
|
||||
|
||||
DIVIDERS = {
|
||||
"cs2_2026": 1498736834656604251,
|
||||
"lol_2026": 1498736949706490017,
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_divider_matches_game_and_year():
|
||||
assert sheets.resolve_divider(
|
||||
"[merged] TipiLAN 2026 CS2 Registration Log", DIVIDERS
|
||||
) == 1498736834656604251
|
||||
assert sheets.resolve_divider(
|
||||
"[merged] TipiLAN 2026 LoL Registration Log", DIVIDERS
|
||||
) == 1498736949706490017
|
||||
|
||||
|
||||
def test_resolve_divider_year_scoped_key_ignores_other_years():
|
||||
assert sheets.resolve_divider("TipiLAN 2025 CS2 Registration Log", DIVIDERS) is None
|
||||
|
||||
|
||||
def test_resolve_divider_prefers_the_most_specific_match():
|
||||
dividers = {"cs2": 111, "cs2_2026": 222}
|
||||
assert sheets.resolve_divider("TipiLAN 2026 CS2 Log", dividers) == 222
|
||||
assert sheets.resolve_divider("TipiLAN 2025 CS2 Log", dividers) == 111
|
||||
|
||||
|
||||
def test_resolve_divider_no_config_or_no_title_is_none():
|
||||
assert sheets.resolve_divider("TipiLAN 2026 CS2 Log", {}) is None
|
||||
assert sheets.resolve_divider("", DIVIDERS) is None
|
||||
|
||||
|
||||
def test_title_survives_notice_rows_below_it():
|
||||
# Real sheets stack the game/year title above single-cell notice rows; the
|
||||
# title's keywords must survive so resolve_divider still matches (regression:
|
||||
# notice rows used to overwrite the title, yielding a None divider).
|
||||
rows = [
|
||||
["TipiLAN 2026 CS2 Registration Log"] + [""] * 4,
|
||||
["This log is updated automatically."] + [""] * 4,
|
||||
["If a team from the Top 32 withdraws, ..."] + [""] * 4,
|
||||
["No", "Team Name", "Lineup (nickname, citizenship)", "", ""],
|
||||
["1", "GENESIS", "kapa (EST)", "", ""],
|
||||
]
|
||||
section = sheets.parse_team_sections(rows)[0]
|
||||
assert "CS2" in section.title and "2026" in section.title
|
||||
assert sheets.resolve_divider(section.title, DIVIDERS) == 1498736834656604251
|
||||
|
||||
|
||||
# --- plan_team_positions (pure role-ordering maths) ------------------------
|
||||
|
||||
def test_plan_moves_teams_directly_under_their_divider():
|
||||
# Ascending = bottom-up, so the Discord UI renders this list reversed.
|
||||
ordered = ["@everyone", "60hz", "999", "Raid", "CS2-2026", "LOL-2026", "Bot"]
|
||||
plan = member_sync.plan_team_positions(
|
||||
ordered, {"Bot"}, {"CS2-2026": ["60hz", "999", "Raid"]}
|
||||
)
|
||||
final = sorted(set(ordered), key=lambda n: plan.get(n, ordered.index(n)))
|
||||
# Teams end up just below the divider, alphabetical downwards in the UI.
|
||||
assert final == ["@everyone", "Raid", "999", "60hz", "CS2-2026", "LOL-2026", "Bot"]
|
||||
|
||||
|
||||
def test_plan_groups_each_divider_separately():
|
||||
ordered = ["@everyone", "Alpha", "Pushing 30s", "CS2-2026", "LOL-2026", "Bot"]
|
||||
plan = member_sync.plan_team_positions(
|
||||
ordered,
|
||||
{"Bot"},
|
||||
{"CS2-2026": ["Alpha"], "LOL-2026": ["Pushing 30s"]},
|
||||
)
|
||||
final = sorted(set(ordered), key=lambda n: plan.get(n, ordered.index(n)))
|
||||
assert final == ["@everyone", "Alpha", "CS2-2026", "Pushing 30s", "LOL-2026", "Bot"]
|
||||
|
||||
|
||||
def test_plan_is_a_noop_when_already_positioned():
|
||||
ordered = ["@everyone", "Raid", "999", "60hz", "CS2-2026", "Bot"]
|
||||
assert member_sync.plan_team_positions(
|
||||
ordered, {"Bot"}, {"CS2-2026": ["60hz", "999", "Raid"]}
|
||||
) == {}
|
||||
|
||||
|
||||
def test_plan_skips_missing_divider_and_leaves_its_teams_alone():
|
||||
ordered = ["@everyone", "60hz", "CS2-2026", "Bot"]
|
||||
plan = member_sync.plan_team_positions(
|
||||
ordered, {"Bot"}, {"LOL-2026": ["60hz"]} # divider not in the guild
|
||||
)
|
||||
assert plan == {}
|
||||
|
||||
|
||||
def test_plan_never_moves_managed_roles():
|
||||
ordered = ["@everyone", "60hz", "CS2-2026", "Nitro", "Bot"]
|
||||
plan = member_sync.plan_team_positions(
|
||||
ordered, {"Nitro", "Bot"}, {"CS2-2026": ["60hz", "Nitro"]}
|
||||
)
|
||||
assert "Nitro" not in plan and "Bot" not in plan
|
||||
|
||||
|
||||
def test_build_username_index_is_case_insensitive():
|
||||
index = sheets.build_username_index({"GENESIS": ["Kapa", "neaQ"]})
|
||||
assert index == {"kapa": "GENESIS", "neaq": "GENESIS"}
|
||||
|
||||
|
||||
# --- sync_team_role behaviour (roster-independent) -------------------------
|
||||
|
||||
class FakeRole:
|
||||
def __init__(self, rid: int, name: str):
|
||||
self.id = rid
|
||||
self.name = name
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, FakeRole) and other.id == self.id
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
class FakeMember:
|
||||
def __init__(self, uid: int, name: str, roles, bot: bool = False):
|
||||
self.id = uid
|
||||
self.name = name
|
||||
self.display_name = name
|
||||
self.bot = bot
|
||||
self.roles = list(roles)
|
||||
|
||||
async def add_roles(self, *roles, reason=None):
|
||||
self.roles.extend(roles)
|
||||
|
||||
async def remove_roles(self, *roles, reason=None):
|
||||
self.roles = [r for r in self.roles if r not in roles]
|
||||
|
||||
|
||||
class FakeGuild:
|
||||
def __init__(self, roles, members=None):
|
||||
self.roles = list(roles)
|
||||
self.members = list(members or [])
|
||||
self._next = 9000
|
||||
self.created: list[str] = []
|
||||
|
||||
async def create_role(self, name, reason=None):
|
||||
self._next += 1
|
||||
role = FakeRole(self._next, name)
|
||||
self.roles.append(role)
|
||||
self.created.append(name)
|
||||
return role
|
||||
|
||||
def get_role(self, rid):
|
||||
return next((r for r in self.roles if r.id == rid), None)
|
||||
|
||||
|
||||
def test_sync_creates_missing_team_role_and_removes_old_one(monkeypatch):
|
||||
old_team = FakeRole(1, "OldTeam")
|
||||
keeper = FakeRole(2, "Member") # not a team role - must be left alone
|
||||
member = FakeMember(1, "tft", roles=[old_team, keeper])
|
||||
guild = FakeGuild([old_team, keeper])
|
||||
|
||||
monkeypatch.setattr(sheets, "get_team_for_username",
|
||||
lambda n: "GENESIS" if n.lower() == "tft" else None)
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: {"GENESIS", "OldTeam"})
|
||||
|
||||
result = run(member_sync.sync_team_role(member, guild))
|
||||
|
||||
assert result.created == "GENESIS" # auto-created the missing role
|
||||
assert "GENESIS" in guild.created
|
||||
assert result.added == "GENESIS"
|
||||
assert result.removed == ["OldTeam"] # left their previous team
|
||||
role_names = {r.name for r in member.roles}
|
||||
assert "GENESIS" in role_names
|
||||
assert "OldTeam" not in role_names
|
||||
assert "Member" in role_names # unrelated role untouched
|
||||
|
||||
|
||||
def test_sync_uses_existing_team_role(monkeypatch):
|
||||
genesis = FakeRole(3, "GENESIS")
|
||||
member = FakeMember(1, "kapa", roles=[])
|
||||
guild = FakeGuild([genesis])
|
||||
|
||||
monkeypatch.setattr(sheets, "get_team_for_username", lambda n: "GENESIS")
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: {"GENESIS"})
|
||||
|
||||
result = run(member_sync.sync_team_role(member, guild))
|
||||
|
||||
assert guild.created == [] # did NOT create a duplicate
|
||||
assert result.created is None
|
||||
assert result.added == "GENESIS"
|
||||
assert genesis in member.roles
|
||||
|
||||
|
||||
def test_sync_grants_game_divider_role(monkeypatch):
|
||||
genesis = FakeRole(3, "GENESIS")
|
||||
cs2_div = FakeRole(100, "====== CS2 2026 ======")
|
||||
member = FakeMember(1, "kapa", roles=[])
|
||||
guild = FakeGuild([genesis, cs2_div])
|
||||
|
||||
monkeypatch.setattr(sheets, "get_team_for_username", lambda n: "GENESIS")
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: {"GENESIS"})
|
||||
monkeypatch.setattr(sheets, "get_team_dividers", lambda: {"GENESIS": 100})
|
||||
monkeypatch.setattr(config, "TEAM_DIVIDERS", {"cs2_2026": 100, "lol_2026": 200})
|
||||
|
||||
result = run(member_sync.sync_team_role(member, guild))
|
||||
|
||||
assert result.divider_added == "====== CS2 2026 ======"
|
||||
assert cs2_div in member.roles
|
||||
|
||||
|
||||
def test_sync_swaps_divider_role_on_game_switch(monkeypatch):
|
||||
genesis = FakeRole(3, "GENESIS")
|
||||
cs2_div = FakeRole(100, "CS2")
|
||||
lol_div = FakeRole(200, "LoL")
|
||||
member = FakeMember(1, "kapa", roles=[lol_div]) # was LoL, now on a CS2 team
|
||||
guild = FakeGuild([genesis, cs2_div, lol_div])
|
||||
|
||||
monkeypatch.setattr(sheets, "get_team_for_username", lambda n: "GENESIS")
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: {"GENESIS"})
|
||||
monkeypatch.setattr(sheets, "get_team_dividers", lambda: {"GENESIS": 100})
|
||||
monkeypatch.setattr(config, "TEAM_DIVIDERS", {"cs2_2026": 100, "lol_2026": 200})
|
||||
|
||||
result = run(member_sync.sync_team_role(member, guild))
|
||||
|
||||
assert result.divider_added == "CS2"
|
||||
assert result.divider_removed == ["LoL"]
|
||||
ids = {r.id for r in member.roles}
|
||||
assert 100 in ids and 200 not in ids
|
||||
|
||||
|
||||
def test_sync_strips_team_role_when_not_registered(monkeypatch):
|
||||
old_team = FakeRole(1, "OldTeam")
|
||||
member = FakeMember(1, "ghost", roles=[old_team])
|
||||
guild = FakeGuild([old_team])
|
||||
|
||||
monkeypatch.setattr(sheets, "get_team_for_username", lambda n: None)
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: {"OldTeam"})
|
||||
|
||||
result = run(member_sync.sync_team_role(member, guild))
|
||||
|
||||
assert result.removed == ["OldTeam"]
|
||||
assert result.added is None
|
||||
assert old_team not in member.roles
|
||||
|
||||
|
||||
def test_sync_no_team_sheet_is_noop(monkeypatch):
|
||||
keeper = FakeRole(2, "Member")
|
||||
member = FakeMember(1, "someone", roles=[keeper])
|
||||
guild = FakeGuild([keeper])
|
||||
|
||||
# Empty caches = feature switched off: no removals even of a stale team role.
|
||||
monkeypatch.setattr(sheets, "get_team_for_username", lambda n: None)
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: set())
|
||||
|
||||
result = run(member_sync.sync_team_role(member, guild))
|
||||
|
||||
assert result.removed == []
|
||||
assert guild.created == []
|
||||
assert keeper in member.roles
|
||||
|
||||
|
||||
def test_sync_all_team_roles_aggregates_and_skips_bots(monkeypatch):
|
||||
genesis = FakeRole(3, "GENESIS")
|
||||
m1 = FakeMember(1, "kapa", roles=[]) # will get GENESIS
|
||||
m2 = FakeMember(2, "nobody", roles=[]) # not registered, unchanged
|
||||
bot_member = FakeMember(3, "botto", roles=[], bot=True) # skipped
|
||||
guild = FakeGuild([genesis], members=[m1, m2, bot_member])
|
||||
|
||||
teams = {"kapa": "GENESIS"}
|
||||
monkeypatch.setattr(sheets, "get_team_for_username", lambda n: teams.get(n.lower()))
|
||||
monkeypatch.setattr(sheets, "all_team_names", lambda: {"GENESIS"})
|
||||
|
||||
summary = run(member_sync.sync_all_team_roles(guild))
|
||||
|
||||
assert summary.scanned == 2 # bot excluded
|
||||
assert summary.assigned == 1
|
||||
assert summary.removed == 0
|
||||
assert summary.changes == ["kapa: +GENESIS"]
|
||||
Reference in New Issue
Block a user