forked from sass/tipibot
feat(teams): add Fienta as primary Discord->team source (sheet fallback)
The registration-log sheet only has in-game nicknames, so matching Discord
users to teams failed for ~10 of 40 teams. Fienta collects each competitor's
Discord username (+ sometimes user ID) and team name per ticket, giving a
reliable Discord-identity -> team mapping (validated: 201 usernames, 42 teams).
- core/fienta.py: token-auth client; fetch /events/{id}/tickets?attendees=true,
parse competitor/coach/substitute tickets into {username|id -> team} and
{team -> game}; exclude visitor/supporter/LAN/early-bird/waiting-list. No-op
when FIENTA_API_TOKEN/FIENTA_EVENT_ID unset.
- member_sync: resolve_team() tries Fienta (id, then username) then the sheet;
all_managed_team_names() and team_dividers() merge both sources.
- /teamsync + hourly task refresh Fienta alongside the sheet; enabled when
either source is configured.
- config + .env.example: FIENTA_API_TOKEN, FIENTA_EVENT_ID.
- tests: fienta parsing (game detection, inclusion rules, id/username mapping).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPRsW4tazVtYi2jNzVQkre
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"
|
||||
Reference in New Issue
Block a user