"""Unit tests for the optional Google Sheets export (no real network/creds).""" import gspread from tatami.classes import Group from tatami.tatami_masterplan import assign_courses, get_after_party_group from tatami.sheets_export import ( OVERVIEW_TITLE, _group_titles, _unique_titles, export_masterplan_to_sheet, ) from conftest import make_participants COURSE_TIMES = { "starter": "18:30", "main": "20:00", "dessert": "22:00", "after_party": "23:30", } class FakeWorksheet: def __init__(self, title: str): self.title = title self.rows: list[list[str]] | None = None def clear(self) -> None: self.rows = None def update(self, rows: list[list[str]]) -> None: self.rows = rows class FakeSpreadsheet: def __init__(self): self._worksheets: dict[str, FakeWorksheet] = {} def worksheet(self, title: str) -> FakeWorksheet: if title not in self._worksheets: raise gspread.WorksheetNotFound(title) return self._worksheets[title] def add_worksheet( self, title: str, rows: int = 200, cols: int = 10 ) -> FakeWorksheet: worksheet = FakeWorksheet(title) self._worksheets[title] = worksheet return worksheet def worksheets(self) -> list[FakeWorksheet]: return list(self._worksheets.values()) def del_worksheet(self, worksheet: FakeWorksheet) -> None: del self._worksheets[worksheet.title] def make_groups(n: int) -> list[Group]: """n one-member groups with starter/main/dessert cycling and hosts wired up.""" groups = [Group(members=[p]) for p in make_participants(n)] courses = ["starter", "main", "dessert"] * (n // 3) assign_courses(groups, courses) return groups class TestUniqueTitles: def test_dedups_repeated_names(self): assert _unique_titles(["Alice", "Bob", "Alice"]) == [ "Alice", "Bob", "Alice (2)", ] def test_leaves_distinct_names_untouched(self): assert _unique_titles(["Alice", "Bob"]) == ["Alice", "Bob"] class TestGroupTitles: def test_joins_all_member_names(self): participants = make_participants(2) group = Group(members=participants) assert _group_titles([group]) == [ f"{participants[0].name} & {participants[1].name}" ] def test_is_not_a_single_members_name(self): participants = make_participants(3) group = Group(members=participants) title = _group_titles([group])[0] assert all(member.name != title for member in participants) class TestExportMasterplanToSheet: def test_writes_overview_with_one_row_per_participant(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) overview = spreadsheet.worksheet(OVERVIEW_TITLE) assert overview.rows is not None rows = overview.rows assert rows[0] == ["Group", "Course", "Name", "Phone", "Address", "Allergies"] member_rows = rows[1:4] assert len(member_rows) == 3 names = {row[2] for row in member_rows} assert names == {g.main_member.name for g in groups} def test_writes_meal_times_table_below_the_group_assignments(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) rows = spreadsheet.worksheet(OVERVIEW_TITLE).rows assert rows is not None times_idx = rows.index(["Meal Times"]) assert rows[times_idx + 1] == ["Course", "Time"] assert rows[times_idx + 2 :] == [ [course, time] for course, time in COURSE_TIMES.items() ] def test_writes_support_contacts_and_info_text_when_given(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() contacts = [ ("Lars (Organizer)", "0151-1234567"), ("Anna (Backup)", "0151-7654321"), ] info_text = "Be on time.\nBring a small gift for your hosts." export_masterplan_to_sheet( spreadsheet, groups, after_party, COURSE_TIMES, organizer_contacts=contacts, info_text=info_text, ) rows = spreadsheet.worksheet(OVERVIEW_TITLE).rows assert rows is not None contacts_idx = rows.index(["Support Contacts"]) assert rows[contacts_idx + 1] == ["Name", "Contact"] assert rows[contacts_idx + 2 : contacts_idx + 4] == [list(c) for c in contacts] info_idx = rows.index(["Info"]) assert rows[info_idx + 1 :] == [ ["Be on time."], ["Bring a small gift for your hosts."], ] def test_omits_support_contacts_and_info_sections_when_not_given(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) rows = spreadsheet.worksheet(OVERVIEW_TITLE).rows assert rows is not None assert ["Support Contacts"] not in rows assert ["Info"] not in rows def test_writes_one_tab_per_group_with_route_and_guests(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) starter_group = next(g for g in groups if g.course == "starter") tab = spreadsheet.worksheet(starter_group.main_member.name) assert tab.rows is not None route_header_idx = tab.rows.index(["Route", "Host", "Address", "Time"]) route_rows = tab.rows[route_header_idx + 1 : route_header_idx + 1 + 4] # 3 hosts (starter, main, dessert) plus the after party. labels = [row[0] for row in route_rows] assert labels == ["starter", "main", "dessert", "After Party"] times = [row[3] for row in route_rows] assert times == ["18:30", "20:00", "22:00", "23:30"] guest_header_idx = tab.rows.index(["Guests for your course", "Allergies"]) guest_rows = tab.rows[guest_header_idx + 1 :] expected_guests = {p.name for p in starter_group.get_guests(groups)} assert {row[0] for row in guest_rows} == expected_guests def test_group_tab_is_titled_after_all_members_not_just_the_host(self): participants = make_participants(6) groups = [Group(members=[participants[0], participants[1]])] groups[0].add_member(participants[2]) groups += [Group(members=[participants[3]]), Group(members=[participants[4]])] groups[1].add_member(participants[5]) assign_courses(groups, ["starter", "main", "dessert"]) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) host_group = groups[0] expected_title = " & ".join(m.name for m in host_group.members) assert expected_title != host_group.main_member.name tab = spreadsheet.worksheet(expected_title) assert tab.rows is not None assert tab.rows[0] == [expected_title] def test_deletes_stale_tabs_from_a_previous_run(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() spreadsheet.add_worksheet("Leftover Tab") export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) assert "Leftover Tab" not in {w.title for w in spreadsheet.worksheets()} def test_rerun_is_idempotent(self): groups = make_groups(3) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) expected_titles = {OVERVIEW_TITLE, *(g.main_member.name for g in groups)} assert {w.title for w in spreadsheet.worksheets()} == expected_titles def test_dedups_tab_names_for_same_named_hosts(self): participants = make_participants(3) participants[1].name = participants[0].name groups = [Group(members=[p]) for p in participants] assign_courses(groups, ["starter", "main", "dessert"]) after_party = get_after_party_group("party street") spreadsheet = FakeSpreadsheet() export_masterplan_to_sheet(spreadsheet, groups, after_party, COURSE_TIMES) titles = {w.title for w in spreadsheet.worksheets()} assert participants[0].name in titles assert f"{participants[0].name} (2)" in titles