"""Unit tests for the slack_notifier handler.""" import sys import os from datetime import datetime from decimal import Decimal from unittest.mock import patch from zoneinfo import ZoneInfo # --------------------------------------------------------------------------- # Ensure the handler module can be imported. The shared layer lives under # src/shared/ and the handler lives under functions/slack_notifier/. # --------------------------------------------------------------------------- import importlib.util _repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(_repo, "src", "shared")) _handler_path = os.path.join(_repo, "functions", "slack_notifier", "handler.py") _spec = importlib.util.spec_from_file_location("slack_notifier_handler", _handler_path) handler = importlib.util.module_from_spec(_spec) sys.modules["slack_notifier_handler"] = handler _spec.loader.exec_module(handler) ET = ZoneInfo("America/New_York") # ──────────────────────────────────────────────────────────────────────────── # Helpers # ──────────────────────────────────────────────────────────────────────────── def _make_datetime(year, month, day, hour, minute=0): """Return a timezone-aware datetime in America/New_York.""" return datetime(year, month, day, hour, minute, tzinfo=ET) def _thursday_10am(): """2026-05-14 is a Thursday.""" return _make_datetime(2026, 5, 14, 10) def _thursday_11am(): return _make_datetime(2026, 5, 14, 11) def _wednesday_10am(): """2026-05-13 is a Wednesday.""" return _make_datetime(2026, 5, 13, 10) # ──────────────────────────────────────────────────────────────────────────── # Reminder Dedup Guard (Critical) # ──────────────────────────────────────────────────────────────────────────── class TestReminderDedupGuard: @patch("slack_notifier_handler.datetime") def test_reminder_skipped_wrong_hour(self, mock_dt): """Invoked at 11am Thursday ET -> returns skipped.""" mock_dt.now.return_value = _thursday_11am() result = handler.handle_reminder({"event": "reminder"}) assert result["status"] == "skipped", "Should skip when hour is not 10" assert "outside reminder window" in result["reason"] @patch("slack_notifier_handler.datetime") def test_reminder_skipped_wrong_day(self, mock_dt): """Invoked at 10am Wednesday ET -> returns skipped.""" mock_dt.now.return_value = _wednesday_10am() result = handler.handle_reminder({"event": "reminder"}) assert result["status"] == "skipped", "Should skip when day is not Thursday" assert "outside reminder window" in result["reason"] @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_orders", return_value=[]) @patch("slack_notifier_handler.get_roster", return_value=[]) @patch("slack_notifier_handler.current_week", return_value="2026-W19") @patch("slack_notifier_handler.datetime") def test_reminder_runs_at_correct_time( self, mock_dt, mock_week, mock_roster, mock_orders, mock_dm ): """Invoked at 10am Thursday ET -> proceeds (does not skip).""" mock_dt.now.return_value = _thursday_10am() result = handler.handle_reminder({"event": "reminder"}) assert result["status"] != "skipped", "Should not skip at 10am Thursday" @patch("slack_notifier_handler.datetime") def test_lambda_handler_reminder_guard(self, mock_dt): """lambda_handler lines 32-34 also return skipped for wrong time.""" mock_dt.now.return_value = _thursday_11am() result = handler.lambda_handler({"event": "reminder"}, None) assert result["status"] == "skipped", ( "lambda_handler should short-circuit before calling handle_reminder" ) assert "outside reminder window" in result["reason"] # ──────────────────────────────────────────────────────────────────────────── # Reminder DMs (High) # ──────────────────────────────────────────────────────────────────────────── class TestReminderDMs: @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_orders") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.current_week", return_value="2026-W19") @patch("slack_notifier_handler.datetime") def test_reminder_sends_to_non_ordered( self, mock_dt, mock_week, mock_roster, mock_orders, mock_dm ): """Roster of 3, 1 has ordered -> DMs sent to 2 others.""" mock_dt.now.return_value = _thursday_10am() mock_roster.return_value = [ {"email": "alice@x.com", "name": "Alice A", "slack_user_id": "U001"}, {"email": "bob@x.com", "name": "Bob B", "slack_user_id": "U002"}, {"email": "carol@x.com", "name": "Carol C", "slack_user_id": "U003"}, ] mock_orders.return_value = [ {"employee_email": "alice@x.com"}, ] result = handler.handle_reminder({"event": "reminder"}) assert result["dm_count"] == 2, "Should DM the 2 employees who haven't ordered" assert result["missing_count"] == 2 assert mock_dm.call_count == 2 dm_user_ids = {call.args[0] for call in mock_dm.call_args_list} assert dm_user_ids == {"U002", "U003"}, "Should DM Bob and Carol, not Alice" @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_orders", return_value=[]) @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.current_week", return_value="2026-W19") @patch("slack_notifier_handler.datetime") def test_reminder_skips_no_slack_id( self, mock_dt, mock_week, mock_roster, mock_orders, mock_dm ): """Employee without slack_user_id is counted as missing but not DM'd.""" mock_dt.now.return_value = _thursday_10am() mock_roster.return_value = [ {"email": "dave@x.com", "name": "Dave D"}, # no slack_user_id {"email": "eve@x.com", "name": "Eve E", "slack_user_id": "U005"}, ] result = handler.handle_reminder({"event": "reminder"}) assert result["missing_count"] == 2, "Both are missing (no orders at all)" assert result["dm_count"] == 1, "Only Eve should be DM'd (Dave has no Slack ID)" mock_dm.assert_called_once() assert mock_dm.call_args.args[0] == "U005" @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_orders") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.current_week", return_value="2026-W19") @patch("slack_notifier_handler.datetime") def test_reminder_case_insensitive_email( self, mock_dt, mock_week, mock_roster, mock_orders, mock_dm ): """'Adam@x.com' in roster matches 'adam@x.com' in orders.""" mock_dt.now.return_value = _thursday_10am() mock_roster.return_value = [ {"email": "Adam@x.com", "name": "Adam M", "slack_user_id": "U010"}, ] mock_orders.return_value = [ {"employee_email": "adam@x.com"}, ] result = handler.handle_reminder({"event": "reminder"}) assert result["dm_count"] == 0, ( "Adam already ordered (case-insensitive match) — no DM expected" ) mock_dm.assert_not_called() # ──────────────────────────────────────────────────────────────────────────── # Order Confirmed (High) # ──────────────────────────────────────────────────────────────────────────── class TestOrderConfirmed: @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_order_confirmed_dm_format(self, mock_week, mock_roster, mock_dm): """DM contains item breakdown with 'your cost' labels and payroll total.""" mock_roster.return_value = [ {"email": "alice@x.com", "name": "Alice Adams", "slack_user_id": "U001"}, ] event = { "event": "order_confirmed", "employee_email": "alice@x.com", "employee_name": "Alice Adams", "items": [ {"name": "Grilled Chicken", "quantity": 2, "price": 8.50}, {"name": "Caesar Salad", "quantity": 1, "price": 6.00}, ], "total": 23.00, "week": "2026-W19", } handler.handle_order_confirmed(event) mock_dm.assert_called_once() call_kwargs = mock_dm.call_args blocks = call_kwargs.kwargs.get("blocks") or call_kwargs[1].get("blocks") body_text = blocks[1]["text"]["text"] assert "your cost: $17.00" in body_text, "Should show Grilled Chicken x2 cost" assert "your cost: $6.00" in body_text, "Should show Caesar Salad x1 cost" assert "$23.00" in body_text, "Should show payroll deduction total" assert "payroll deduction" in body_text.lower() @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_order_confirmed_no_slack_id(self, mock_week, mock_roster, mock_dm): """Employee not in roster -> returns {'status': 'no_slack_id'}.""" mock_roster.return_value = [] # empty roster event = { "event": "order_confirmed", "employee_email": "nobody@x.com", "employee_name": "Nobody", "items": [], "total": 0, } result = handler.handle_order_confirmed(event) assert result["status"] == "no_slack_id" mock_dm.assert_not_called() @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_order_confirmed_empty_name(self, mock_week, mock_roster, mock_dm): """Empty name -> greeting says 'Hey there!' not crash.""" mock_roster.return_value = [ {"email": "anon@x.com", "name": "", "slack_user_id": "U099"}, ] event = { "event": "order_confirmed", "employee_email": "anon@x.com", "employee_name": "", "items": [{"name": "Soup", "quantity": 1, "price": 5.00}], "total": 5.00, } result = handler.handle_order_confirmed(event) assert result["status"] == "confirmed", "Should not crash on empty name" blocks = mock_dm.call_args.kwargs.get("blocks") or mock_dm.call_args[1].get( "blocks" ) body_text = blocks[1]["text"]["text"] assert "Hey there!" in body_text, ( "Should greet with 'Hey there!' when name is empty" ) # ──────────────────────────────────────────────────────────────────────────── # Orders Aggregated (High) # ──────────────────────────────────────────────────────────────────────────── class TestOrdersAggregated: @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.get_settings") @patch("slack_notifier_handler.get_summary") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_orders_aggregated_with_subsidy( self, mock_week, mock_summary, mock_settings, mock_roster, mock_dm ): """employee_total < grand_total -> admin DM includes company subsidy line.""" mock_summary.return_value = { "total_employees": 5, "total_meals": 12, "grand_total": Decimal("150.00"), "employee_total": Decimal("120.00"), "meals": [ {"meal": "Grilled Chicken", "quantity": Decimal("7")}, {"meal": "Veggie Bowl", "quantity": Decimal("5")}, ], } mock_settings.return_value = {"admin_emails": ["admin@x.com"]} mock_roster.return_value = [ {"email": "admin@x.com", "name": "Admin A", "slack_user_id": "U100"}, ] result = handler.handle_orders_aggregated({"event": "orders_aggregated"}) assert result["status"] == "notified" mock_dm.assert_called_once() blocks = mock_dm.call_args.args[2] section_text = blocks[1]["text"]["text"] assert "$150.00" in section_text, "Should show grand total" assert "$120.00" in section_text, "Should show employee payroll deductions" assert "$30.00" in section_text, "Should show company subsidy amount" assert "company subsidy" in section_text.lower() @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.get_settings") @patch("slack_notifier_handler.get_summary") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_orders_aggregated_without_subsidy( self, mock_week, mock_summary, mock_settings, mock_roster, mock_dm ): """Equal totals -> no subsidy line.""" mock_summary.return_value = { "total_employees": 3, "total_meals": 6, "grand_total": Decimal("90.00"), "employee_total": Decimal("90.00"), "meals": [ {"meal": "Pasta Primavera", "quantity": Decimal("6")}, ], } mock_settings.return_value = {"admin_emails": ["admin@x.com"]} mock_roster.return_value = [ {"email": "admin@x.com", "name": "Admin A", "slack_user_id": "U100"}, ] result = handler.handle_orders_aggregated({"event": "orders_aggregated"}) assert result["status"] == "notified" blocks = mock_dm.call_args.args[2] section_text = blocks[1]["text"]["text"] assert "company subsidy" not in section_text.lower(), ( "Should not mention subsidy when employee_total == grand_total" ) @patch("slack_notifier_handler.post_channel_message") @patch("slack_notifier_handler.get_summary") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_orders_aggregated_no_summary(self, mock_week, mock_summary, mock_post): """No summary in DB -> returns {'status': 'no_summary'}.""" mock_summary.return_value = None result = handler.handle_orders_aggregated({"event": "orders_aggregated"}) assert result["status"] == "no_summary" mock_post.assert_not_called() @patch("slack_notifier_handler.send_dm") @patch("slack_notifier_handler.get_roster") @patch("slack_notifier_handler.get_settings") @patch("slack_notifier_handler.post_channel_message") @patch("slack_notifier_handler.get_summary") @patch("slack_notifier_handler.current_week", return_value="2026-W19") def test_orders_aggregated_dms_admins( self, mock_week, mock_summary, mock_post, mock_settings, mock_roster, mock_dm ): """Admins with a Slack ID get DM'd the summary; others are skipped.""" mock_summary.return_value = { "total_employees": 2, "total_meals": 4, "grand_total": Decimal("60.00"), "employee_total": Decimal("60.00"), "meals": [ {"meal": "Grilled Chicken", "quantity": Decimal("4")}, ], } mock_settings.return_value = { "admin_emails": ["Admin@x.com", "noslack@x.com", "missing@x.com"], } mock_roster.return_value = [ {"email": "admin@x.com", "name": "Admin A", "slack_user_id": "U100"}, {"email": "noslack@x.com", "name": "No Slack"}, # no slack_user_id ] result = handler.handle_orders_aggregated({"event": "orders_aggregated"}) assert result["status"] == "notified" assert result["admin_dm_count"] == 1, "Only the admin with a Slack ID is DM'd" mock_dm.assert_called_once() assert mock_dm.call_args.args[0] == "U100" # ──────────────────────────────────────────────────────────────────────────── # Escaping (Low) # ──────────────────────────────────────────────────────────────────────────── class TestEscaping: def test_escape_mrkdwn(self): """'A & B ' -> 'A & B <C>'.""" assert handler._escape_mrkdwn("A & B ") == "A & B <C>" # ──────────────────────────────────────────────────────────────────────────── # Routing # ──────────────────────────────────────────────────────────────────────────── class TestRouting: def test_unknown_event_type(self): """Unknown event type returns error message.""" result = handler.lambda_handler({"event": "bogus_event"}, None) assert "error" in result, "Should return an error key for unknown event type" assert "bogus_event" in result["error"], ( "Error message should include the unrecognised event type" )