"""publish_menu writes the menu, then the form, then Slack.""" from unittest.mock import MagicMock, patch import pytest from server.jobs import publish_menu from server.jobs import run_job MENU = { "scraped_at": "2026-09-21T07:30:00-04:00", "menu_url": "https://www.redefinemeals.com/menu", "meal_count": 1, "meals": [{"name": "Korean Steak Bowl", "price": 12.49}], } ENV = { "MENU_URL": "https://www.redefinemeals.com/menu", "GOOGLE_CLIENT_ID": "client.apps.googleusercontent.com", "FORM_BUCKET": "meal-order-manager-form-test", "DISTRIBUTION_ID": "E123", "FORM_URL": "https://orders.seahaven.com", "TABLE_NAME": "meal-order-manager-orders", } def _clients(s3, cloudfront): def client(name, **_kwargs): if name == "s3": return s3 if name == "cloudfront": return cloudfront raise AssertionError(name) return client @patch("shared.slack.post_channel_message", return_value={"ok": True}) @patch("server.jobs.publish_menu.put_menu") @patch( "server.jobs.publish_menu.get_settings", return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 50}, ) @patch("server.jobs.publish_menu.generate_form", return_value="form") @patch("server.jobs.publish_menu.current_week", return_value="2026-W38") @patch("server.jobs.publish_menu.fetch_menu", return_value=MENU) @patch("server.jobs.publish_menu.boto3.client") def test_publish_menu_uploads_after_the_menu_write( mock_client, mock_fetch, mock_week, mock_generate, mock_settings, mock_put, mock_slack, ): s3 = MagicMock() cloudfront = MagicMock() mock_client.side_effect = _clients(s3, cloudfront) order = [] mock_put.side_effect = lambda *args, **kwargs: order.append("menu") s3.put_object.side_effect = lambda **kwargs: order.append(kwargs["Key"]) cloudfront.create_invalidation.side_effect = lambda **kwargs: order.append( "invalidate" ) mock_slack.side_effect = lambda *args, **kwargs: ( order.append("slack") or {"ok": True} ) with patch.dict("os.environ", ENV, clear=False): result = publish_menu.lambda_handler({}, None) assert result == {"status": "published", "week": "2026-W38", "meal_count": 1} mock_fetch.assert_called_once_with("https://www.redefinemeals.com/menu") mock_week.assert_called_once() mock_settings.assert_called_once() mock_generate.assert_called_once() assert mock_generate.call_args.kwargs["week"] == "2026-W38" assert mock_generate.call_args.kwargs["google_client_id"] == ENV["GOOGLE_CLIENT_ID"] assert mock_generate.call_args.kwargs["bulk_discount"] == 10 assert mock_generate.call_args.kwargs["company_subsidy"] == 50 mock_put.assert_called_once_with("2026-W38", MENU) assert s3.put_object.call_count == 2 index = s3.put_object.call_args_list[0].kwargs archive = s3.put_object.call_args_list[1].kwargs assert index["Bucket"] == ENV["FORM_BUCKET"] assert index["Key"] == "index.html" assert index["CacheControl"] == "no-cache" assert archive["Key"] == "archive/2026-W38.html" invalidation = cloudfront.create_invalidation.call_args.kwargs assert invalidation["DistributionId"] == "E123" assert invalidation["InvalidationBatch"]["Paths"]["Items"] == ["/index.html"] mock_slack.assert_called_once() assert mock_slack.call_args.args[0] == ( "This week's meal order is open! Deadline: Thursday at 11:59 PM." ) assert mock_slack.call_args.args[1][1]["text"]["text"] == ( f"*<{ENV['FORM_URL']}|Place your order>*\n\n*Deadline:* Thursday at 11:59 PM\n" ) assert order == [ "menu", "index.html", "archive/2026-W38.html", "invalidate", "slack", ] @patch("shared.slack.post_channel_message", return_value={"ok": True}) @patch("server.jobs.publish_menu.put_menu") @patch("server.jobs.publish_menu.get_settings", return_value={}) @patch("server.jobs.publish_menu.generate_form", return_value="form") @patch("server.jobs.publish_menu.current_week", return_value="2026-W38") @patch("server.jobs.publish_menu.fetch_menu", return_value=MENU) @patch("server.jobs.publish_menu.boto3.client") def test_publish_menu_does_not_notify_when_upload_fails( mock_client, _fetch, _week, _generate, _settings, mock_put, mock_slack, ): s3 = MagicMock() s3.put_object.side_effect = RuntimeError("s3 down") mock_client.side_effect = _clients(s3, MagicMock()) with patch.dict("os.environ", ENV, clear=False): with pytest.raises(RuntimeError, match="s3 down"): publish_menu.lambda_handler({}, None) mock_put.assert_called_once() mock_slack.assert_not_called() def test_run_job_dispatches_publish_menu(): with patch( "server.jobs.publish_menu.lambda_handler", return_value={"status": "published"} ) as handler: result = run_job({"event": "publish_menu"}) assert result == {"status": "published"} handler.assert_called_once()