"""Tests for shared.blocks — Block Kit builders.""" from freezegun import freeze_time from shared.blocks import ( build_admin_overview, build_help_blocks, build_holiday_add_modal, build_holiday_added_blocks, build_override_modal, build_pay_summary_blocks, build_pickup_request_blocks, build_pickup_resolved_blocks, build_roster_blocks, build_shift_change_message, build_swap_request_blocks, build_swap_resolved_blocks, build_week_schedule, markdown_to_mrkdwn, ) def _all_action_ids(blocks): ids = [] for b in blocks: if b.get("type") == "actions": ids.extend(e["action_id"] for e in b["elements"]) return ids class TestBuildWeekSchedule: @freeze_time("2026-06-01 12:00:00") # Monday def test_header_and_section_present(self, schedule): blocks = build_week_schedule(schedule) assert blocks[0]["type"] == "header" assert "After-Hours Schedule" in blocks[0]["text"]["text"] assert blocks[1]["type"] == "section" @freeze_time("2026-06-01 12:00:00") def test_all_available_produces_pickup_buttons(self, schedule): # Empty schedule → every shift is available → pickup buttons exist, # including a weekend day button with the _day suffix. blocks = build_week_schedule(schedule) action_ids = _all_action_ids(blocks) assert "pickup_2026-06-03" in action_ids # Wednesday night assert "pickup_2026-06-06_day" in action_ids # Saturday day shift assert "pickup_2026-06-06" in action_ids # Saturday night shift @freeze_time("2026-06-01 12:00:00") def test_assigned_shift_has_no_pickup_button(self, schedule, seed): seed.weekly("Wednesday", "114", "Alice") blocks = build_week_schedule(schedule) assert "pickup_2026-06-03" not in _all_action_ids(blocks) assert "Alice (Ext 114)" in blocks[1]["text"]["text"] @freeze_time("2026-06-01 12:00:00") def test_weekday_night_row_shows_glyph_without_time_label(self, schedule, seed): # Weekdays are night-only, so the moon glyph alone differentiates the row; # the verbose time-range label is reserved for weekends where both shifts # coexist (and would otherwise repeat on ~10 weekday rows). seed.weekly("Wednesday", "114", "Alice") text = build_week_schedule(schedule)[1]["text"]["text"] wednesday_line = next( line for line in text.splitlines() if "Alice (Ext 114)" in line ) assert ":crescent_moon:" in wednesday_line assert "Night (5pm" not in wednesday_line @freeze_time("2026-06-01 12:00:00") def test_weekend_night_row_keeps_glyph_and_time_label(self, schedule, seed): # Weekends carry both shifts, so the night row keeps the full label. seed.weekly("Saturday", "200", "Alice") text = build_week_schedule(schedule)[1]["text"]["text"] saturday_line = next( line for line in text.splitlines() if "Alice (Ext 200)" in line ) assert ":crescent_moon:" in saturday_line assert "Night (5pm" in saturday_line @freeze_time("2026-06-01 12:00:00") def test_weekend_day_row_shows_day_glyph_and_label(self, schedule, seed): seed.weekly("Saturday", "200", "Alice", shift_type="day") text = build_week_schedule(schedule)[1]["text"]["text"] assert ":sunny:" in text assert "Day (8am" in text @freeze_time("2026-06-01 12:00:00") def test_holiday_on_weekday_renders_badge_and_open_slots(self, schedule, seed): # A holiday can land on a weekday (here a Thursday) and shows the badge, # the multiplier, and a day-shift pickup button for its open slots. seed.holiday("2026-06-04", slots=2, label="Test Holiday") blocks = build_week_schedule(schedule) text = blocks[1]["text"]["text"] assert "Holiday" in text assert "Test Holiday" in text assert "1.5x" in text assert "2 open slots" in text assert "pickup_2026-06-04_day" in _all_action_ids(blocks) @freeze_time("2026-06-01 12:00:00") def test_holiday_lists_assignees_and_remaining_slots(self, schedule, seed): seed.holiday( "2026-06-04", slots=2, label="Test Holiday", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) blocks = build_week_schedule(schedule) text = blocks[1]["text"]["text"] assert "Alice (Ext 114)" in text assert "1 open slot" in text # one of two slots filled @freeze_time("2026-06-01 12:00:00") def test_full_holiday_has_no_pickup_button(self, schedule, seed): seed.holiday( "2026-06-04", slots=1, label="Test Holiday", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) blocks = build_week_schedule(schedule) assert "pickup_2026-06-04_day" not in _all_action_ids(blocks) @freeze_time("2026-06-01 12:00:00") def test_holiday_pickup_button_label_says_holiday(self, schedule, seed): seed.holiday("2026-06-04", slots=2, label="Test Holiday") blocks = build_week_schedule(schedule) labels = [ e["text"]["text"] for b in blocks if b.get("type") == "actions" for e in b["elements"] ] assert any("Holiday" in lbl for lbl in labels) class TestBuildShiftChangeMessage: def test_picked_up_weekday(self): blocks = build_shift_change_message( "U1", "2026-06-03", "picked_up", "114", "Alice" ) text = blocks[0]["text"]["text"] assert "<@U1>" in text and "picked up" in text and "Ext 114" in text # Weekday is night-only → the notification still labels the after-hours shift assert "Night (5pm" in text def test_dropped_shows_available(self): blocks = build_shift_change_message( "U1", "2026-06-03", "dropped", "114", "Alice" ) assert "Available" in blocks[0]["text"]["text"] def test_swapped_text(self): blocks = build_shift_change_message("U2", "2026-06-03", "swapped", "115", "Bob") assert "swapped" in blocks[0]["text"]["text"] def test_weekend_includes_shift_label(self): blocks = build_shift_change_message( "U1", "2026-06-06", "picked_up", "200", "Alice", shift_type="day" ) assert "Day (8am" in blocks[0]["text"]["text"] class TestBuildPaySummaryBlocks: def test_renders_breakdown_and_totals(self): breakdown = [ { "day": "Mon", "date_label": "Jun 1", "name": "Alice", "extension": "114", "rate": 50.0, } ] totals = { "Alice": {"shifts": 1, "total": 50.0, "extension": "114", "rate": 50.0} } blocks = build_pay_summary_blocks("Jun 1 to Jun 7", breakdown, totals) assert blocks[0]["type"] == "header" assert "Jun 1 to Jun 7" in blocks[0]["text"]["text"] text = blocks[1]["text"]["text"] assert "Alice" in text and "$50.00" in text and "1 shift" in text def test_holiday_row_shows_effective_rate_and_note(self): breakdown = [ { "day": "Thu", "date_label": "Jul 4", "name": "Alice", "extension": "114", "rate": 75.0, # effective (1.5x of 50) "is_holiday": True, "multiplier": 1.5, } ] totals = { "Alice": { "shifts": 1, "total": 75.0, "extension": "114", "rate": 50.0, "holiday_shifts": 1, } } blocks = build_pay_summary_blocks("Jun 29 to Jul 5", breakdown, totals) text = blocks[1]["text"]["text"] assert "$75.00" in text # effective rate on the breakdown line assert "1.5x" in text assert "Holiday" in text assert "incl. 1 holiday" in text # totals note def test_regular_row_has_no_holiday_decorations(self): breakdown = [ { "day": "Mon", "date_label": "Jun 1", "name": "Alice", "extension": "114", "rate": 50.0, } ] totals = { "Alice": {"shifts": 1, "total": 50.0, "extension": "114", "rate": 50.0} } text = build_pay_summary_blocks("Jun 1 to Jun 7", breakdown, totals)[1]["text"][ "text" ] assert "Holiday" not in text assert "incl." not in text class TestBuildSwapRequestBlocks: def _action_ids(self, blocks): return [ e["action_id"] for b in blocks if b["type"] == "actions" for e in b["elements"] ] def test_weekday_request_has_accept_decline(self): blocks = build_swap_request_blocks("U_REQ", "2026-06-03", "night") assert "<@U_REQ>" in blocks[0]["text"]["text"] ids = self._action_ids(blocks) assert ids == ["swap_accept_2026-06-03", "swap_decline_2026-06-03"] def test_weekend_day_request_uses_day_suffix_and_label(self): blocks = build_swap_request_blocks("U_REQ", "2026-06-06", "day") assert "Day (8am" in blocks[0]["text"]["text"] ids = self._action_ids(blocks) assert ids == ["swap_accept_2026-06-06_day", "swap_decline_2026-06-06_day"] def test_button_styles(self): elements = build_swap_request_blocks("U_REQ", "2026-06-03", "night")[1][ "elements" ] assert elements[0]["style"] == "primary" # Accept assert elements[1]["style"] == "danger" # Decline class TestBuildSwapResolvedBlocks: def test_renders_text_no_buttons(self): blocks = build_swap_resolved_blocks("All done.") assert blocks == [ {"type": "section", "text": {"type": "mrkdwn", "text": "All done."}} ] class TestBuildHolidayAddedBlocks: def test_header_context_and_section(self): blocks = build_holiday_added_blocks("2026-07-04", "Independence Day", 2, 1.5) assert blocks[0]["type"] == "header" assert "Independence Day" in blocks[0]["text"]["text"] assert blocks[1]["type"] == "context" section = blocks[-1]["text"]["text"] assert "2 slots" in section assert "1.5x" in section assert "Holiday" in section def test_single_slot_is_singular(self): section = build_holiday_added_blocks("2026-07-04", "Indep", 1, 2)[-1]["text"][ "text" ] assert "1 slot" in section and "1 slots" not in section assert "2x" in section # whole-number multiplier drops the .0 def test_date_label_override(self): blocks = build_holiday_added_blocks( "2026-07-04", "Indep", 1, 1.5, date_label="July 4th" ) assert blocks[1]["elements"][0]["text"] == "July 4th" class TestBuildPickupRequestBlocks: def _action_ids(self, blocks): return [ e["action_id"] for b in blocks if b["type"] == "actions" for e in b["elements"] ] def test_weekday_request_encodes_date_and_ext(self): blocks = build_pickup_request_blocks( "U_REQ", "Alice", "2026-06-03", "night", "114" ) text = blocks[0]["text"]["text"] assert "<@U_REQ>" in text and "Ext 114" in text assert "already started" in text ids = self._action_ids(blocks) assert ids == ["pickup_approve_2026-06-03_114", "pickup_deny_2026-06-03_114"] def test_weekend_day_request_uses_day_suffix(self): blocks = build_pickup_request_blocks( "U_REQ", "Alice", "2026-06-06", "day", "114" ) assert "Day (8am" in blocks[0]["text"]["text"] ids = self._action_ids(blocks) assert ids == [ "pickup_approve_2026-06-06_day_114", "pickup_deny_2026-06-06_day_114", ] def test_holiday_request_shows_badge_and_day_suffix(self): blocks = build_pickup_request_blocks( "U_REQ", "Alice", "2026-07-04", "day", "114", is_holiday=True ) assert "Holiday" in blocks[0]["text"]["text"] ids = self._action_ids(blocks) assert ids == [ "pickup_approve_2026-07-04_day_114", "pickup_deny_2026-07-04_day_114", ] def test_button_styles(self): elements = build_pickup_request_blocks( "U_REQ", "Alice", "2026-06-03", "night", "114" )[1]["elements"] assert elements[0]["style"] == "primary" # Approve assert elements[1]["style"] == "danger" # Deny class TestBuildPickupResolvedBlocks: def test_renders_text_no_buttons(self): blocks = build_pickup_resolved_blocks("Approved.") assert blocks == [ {"type": "section", "text": {"type": "mrkdwn", "text": "Approved."}} ] class TestBuildHelpBlocks: def test_non_admin_excludes_admin_section(self): text = build_help_blocks(is_admin=False)[0]["text"]["text"] assert "Admin Commands" not in text def test_admin_includes_admin_section(self): text = build_help_blocks(is_admin=True)[0]["text"]["text"] assert "Admin Commands" in text def test_admin_includes_holiday_commands(self): text = build_help_blocks(is_admin=True)[0]["text"]["text"] assert "holiday add" in text assert "holiday remove" in text assert "holiday list" in text def test_non_admin_excludes_holiday_commands(self): text = build_help_blocks(is_admin=False)[0]["text"]["text"] assert "holiday add" not in text class TestBuildRosterBlocks: def test_empty_roster(self): text = build_roster_blocks([])[0]["text"]["text"] assert "No employees" in text def test_linked_and_unlinked(self): roster = [ {"SK": "115", "name": "Bob", "slack_user_id": ""}, {"SK": "114", "name": "Alice", "slack_user_id": "U_ALICE"}, ] text = build_roster_blocks(roster)[0]["text"]["text"] # Sorted by extension → Alice (114) appears before Bob (115) assert text.index("Alice") < text.index("Bob") assert "<@U_ALICE>" in text assert "_not linked_" in text def _block_ids(view): return {b["block_id"] for b in view["blocks"] if b.get("block_id")} class TestBuildOverrideModal: def test_callback_and_input_blocks(self): view = build_override_modal([{"extension": "114", "name": "Alice"}]) assert view["type"] == "modal" assert view["callback_id"] == "admin_override_submit" assert _block_ids(view) == {"date", "extension", "shift_type"} def test_extension_dropdown_built_from_roster(self): view = build_override_modal( [{"extension": "114", "name": "Alice"}, {"extension": "115", "name": "Bob"}] ) ext_block = next(b for b in view["blocks"] if b["block_id"] == "extension") values = [o["value"] for o in ext_block["element"]["options"]] assert values == ["114", "115"] def test_empty_roster_omits_options(self): view = build_override_modal([]) ext_block = next(b for b in view["blocks"] if b["block_id"] == "extension") assert "options" not in ext_block["element"] class TestBuildHolidayAddModal: def test_callback_and_input_blocks(self): view = build_holiday_add_modal() assert view["callback_id"] == "admin_holiday_add_submit" assert _block_ids(view) == {"date", "slots", "multiplier", "label"} def test_multiplier_is_optional(self): view = build_holiday_add_modal() mult = next(b for b in view["blocks"] if b["block_id"] == "multiplier") assert mult["optional"] is True class TestBuildAdminOverview: def test_empty_overview(self): text = build_admin_overview([], [])["text"]["text"] assert "Nothing scheduled" in text def test_lists_overrides_and_holidays(self): overrides = [ { "date": "2026-12-25", "shift_type": "night", "extension": "114", "name": "Alice", }, { "date": "2026-12-26", "shift_type": "day", "extension": "OPEN", "name": "Open", }, ] holidays = [ { "SK": "2026-12-25", "slots": 2, "assignees": {}, "multiplier": 1.5, "label": "Christmas", } ] text = build_admin_overview(overrides, holidays)["text"]["text"] assert "Alice (Ext 114)" in text assert "(Day) — Open" in text assert "Christmas" in text assert "0/2 filled" in text class TestMarkdownToMrkdwn: def test_markdown_bold_becomes_slack_bold(self): assert markdown_to_mrkdwn("**Big news.** text") == "*Big news.* text" def test_markdown_italic_becomes_underscore(self): # Single asterisks are italic in Markdown; Slack uses underscores. assert markdown_to_mrkdwn("a *note* here") == "a _note_ here" def test_bold_and_italic_together(self): out = markdown_to_mrkdwn("**Bold.** Then *(an aside)*") assert out == "*Bold.* Then _(an aside)_" def test_links_and_bullets(self): out = markdown_to_mrkdwn("- see [docs](http://x)\n- next") assert "• see " in out assert "• next" in out def test_adversarial_input_runs_in_linear_time(self): # py/polynomial-redos regression: possessive quantifiers must keep these # patterns from catastrophic backtracking. Pathological inputs that would # hang a backtracking engine complete effectively instantly here. import time for evil in ("*" + "*a" * 4000, "[" + "[\\(" * 4000, "[" + "](" * 4000): start = time.perf_counter() markdown_to_mrkdwn(evil) assert time.perf_counter() - start < 1.0