"""Unit tests for src/server/http_api.py All external dependencies (DynamoDB, SSM, Google tokeninfo, Lambda invoke) are mocked — no real AWS calls are made. """ import json import os import sys import time import urllib.error from datetime import datetime from decimal import Decimal from unittest.mock import MagicMock, patch from zoneinfo import ZoneInfo import pytest # --------------------------------------------------------------------------- # Environment variables required by the handler at import time # --------------------------------------------------------------------------- os.environ.setdefault("TABLE_NAME", "test-orders-table") os.environ.setdefault( "SLACK_NOTIFIER_ARN", "arn:aws:lambda:us-east-1:000000000000:function:test-slack-notifier", ) os.environ.setdefault("GOOGLE_CLIENT_ID_PARAM", "") import importlib.util _handler_path = os.path.join( os.path.dirname(__file__), os.pardir, "src", "server", "http_api.py" ) _spec = importlib.util.spec_from_file_location( "submit_order_handler", os.path.abspath(_handler_path) ) submit_order_handler = importlib.util.module_from_spec(_spec) sys.modules["submit_order_handler"] = submit_order_handler _spec.loader.exec_module(submit_order_handler) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- EASTERN = ZoneInfo("America/New_York") VALID_GOOGLE_CLIENT_ID = "test-google-client-id" def _make_event( method="POST", path="/submit", body=None, headers=None, path_parameters=None, ): """Build an API Gateway v2 HTTP-format event.""" event = { "requestContext": { "http": { "method": method, "path": path, } }, "rawPath": path, "headers": headers or {}, "body": json.dumps(body) if body is not None else "{}", "pathParameters": path_parameters or {}, } return event def _submit_event( items, employee_name="Test User", employee_email="test.user@seahavenind.com", google_token="valid-google-token", extra_body=None, ): """Shortcut for a typical POST /submit event with items.""" body = { "employee_name": employee_name, "employee_email": employee_email, "items": items, } if google_token is not None: body["google_id_token"] = google_token if extra_body: body.update(extra_body) return _make_event( method="POST", path="/submit", body=body, ) def _parse_response(result): """Parse the Lambda response dict into (status_code, body_dict).""" return result["statusCode"], json.loads(result["body"]) def _make_items(retail_prices_and_qtys): """Build item list from [(retail_price, quantity), ...].""" items = [] for i, (price, qty) in enumerate(retail_prices_and_qtys): items.append( { "name": f"Meal {i + 1}", "retail_price": price, "quantity": qty, } ) return items def _menu_doc_from_retail_pairs(pairs): """Dynamo-style menu document matching _make_items retail pairs.""" return { "meals": [ {"name": f"Meal {i + 1}", "price": price} for i, (price, _) in enumerate(pairs) ] } # =========================================================================== # WEEKLY MENU PUBLICATION # =========================================================================== @patch( "submit_order_handler.get_settings", return_value={ "bulk_discount_percent": 10, "company_subsidy_percent": 50, "admin_emails": ["admin@seahavenind.com"], }, ) def test_publish_settings_returns_only_pricing(mock_settings): result = submit_order_handler.lambda_handler( _make_event(method="GET", path="/api/publish/settings"), None ) status, body = _parse_response(result) assert status == 200 assert body == { "bulk_discount_percent": 10.0, "company_subsidy_percent": 50.0, } @patch.dict(os.environ, {"PUBLISH_KEY_PARAM": "/meal-order-manager/publish-key"}) @patch("submit_order_handler.get_parameter", return_value="publish-secret") def test_publish_settings_rejects_missing_key(mock_param): status, body = _parse_response( submit_order_handler.lambda_handler( _make_event(method="GET", path="/api/publish/settings"), None ) ) assert status == 403 assert body == {"error": "Forbidden"} @patch.dict(os.environ, {"PUBLISH_KEY_PARAM": "/meal-order-manager/publish-key"}) @patch("submit_order_handler.get_parameter", return_value="publish-secret") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 50}, ) def test_publish_settings_accepts_matching_key(mock_settings, mock_param): status, body = _parse_response( submit_order_handler.lambda_handler( _make_event( method="GET", path="/api/publish/settings", headers={"X-Meals-Publish-Key": "publish-secret"}, ), None, ) ) assert status == 200 assert body["bulk_discount_percent"] == 10.0 @patch.dict(os.environ, {"STAGE": "prod"}) def test_submit_throttle_returns_429_when_tokens_exhausted(): original_tokens = submit_order_handler._submit_tokens original_last = submit_order_handler._submit_last submit_order_handler._submit_tokens = 0.0 submit_order_handler._submit_last = time.monotonic() try: status, body = _parse_response( submit_order_handler.lambda_handler(_submit_event([{"name": "x"}]), None) ) finally: submit_order_handler._submit_tokens = original_tokens submit_order_handler._submit_last = original_last assert status == 429 assert body == {"error": "Rate limit exceeded"} @patch("submit_order_handler.put_menu") @patch("submit_order_handler.current_week", return_value="2026-W30") def test_publish_menu_uses_current_week_and_server_meal_count(mock_week, mock_put): menu = { "scraped_at": "2026-07-27T07:31:00-04:00", "menu_url": "https://example.com/menu", "meal_count": 999, "meals": [{"name": "Chicken Bowl", "price": 12.5}], } result = submit_order_handler.lambda_handler( _make_event(method="POST", path="/api/publish/menu", body=menu), None ) status, body = _parse_response(result) assert status == 200 assert body == {"status": "published", "week": "2026-W30", "meal_count": 1} mock_put.assert_called_once_with( "2026-W30", { "scraped_at": menu["scraped_at"], "menu_url": menu["menu_url"], "meal_count": 1, "meals": menu["meals"], }, ) @pytest.mark.parametrize( "body", [ {}, {"meals": []}, {"meals": [{"name": "", "price": 12.5}]}, {"meals": [{"name": "Chicken Bowl", "price": -1}]}, {"meals": [{"name": "Chicken Bowl", "price": "12.50"}]}, ], ) @patch("submit_order_handler.put_menu") def test_publish_menu_rejects_invalid_payload(mock_put, body): result = submit_order_handler.lambda_handler( _make_event(method="POST", path="/api/publish/menu", body=body), None ) status, _ = _parse_response(result) assert status == 400 mock_put.assert_not_called() @patch("submit_order_handler.put_menu") def test_publish_menu_rejects_invalid_json(mock_put): event = _make_event(method="POST", path="/api/publish/menu") event["body"] = "{" status, _ = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 400 mock_put.assert_not_called() # =========================================================================== # PUBLIC MENU # =========================================================================== @patch( "submit_order_handler.get_settings", return_value={ "bulk_discount_percent": Decimal("10"), "company_subsidy_percent": Decimal("50"), "order_deadline": "Thursday at 11:59 PM", }, ) @patch("submit_order_handler.get_menu") @patch("submit_order_handler.current_week", return_value="2026-W37") def test_menu_current_returns_portal_payload(mock_week, mock_get_menu, mock_settings): mock_get_menu.return_value = { "form_status": "open", "scraped_at": "2026-09-14T07:30:00-04:00", "menu_url": "https://example.com/menu", "meal_count": Decimal("1"), "meals": [{"name": "Chicken Bowl", "price": Decimal("12.50")}], } event = _make_event( method="GET", path="/api/menu/current", path_parameters={"week": "current"}, ) status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 200 assert body == { "week": "2026-W37", "form_status": "open", "scraped_at": "2026-09-14T07:30:00-04:00", "menu_url": "https://example.com/menu", "meal_count": 1, "meals": [{"name": "Chicken Bowl", "price": 12.5}], "bulk_discount_percent": 10.0, "company_subsidy_percent": 50.0, "order_deadline": "Thursday at 11:59 PM", } mock_get_menu.assert_called_once_with("2026-W37") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch("submit_order_handler.get_menu") def test_menu_explicit_week_uses_safe_defaults(mock_get_menu, mock_settings): mock_get_menu.return_value = {"meals": [{"name": "Soup", "price": 8}]} event = _make_event( method="GET", path="/api/menu/2026-W36", path_parameters={"week": "2026-W36"}, ) status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 200 assert body["week"] == "2026-W36" assert body["form_status"] == "closed" assert body["meal_count"] == 1 assert body["order_deadline"] == "Thursday at 11:59 PM" mock_get_menu.assert_called_once_with("2026-W36") @patch("submit_order_handler.get_settings") @patch("submit_order_handler.get_menu", return_value=None) def test_menu_missing_returns_404(mock_get_menu, mock_settings): event = _make_event( method="GET", path="/api/menu/2026-W35", path_parameters={"week": "2026-W35"}, ) status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 404 assert body == {"error": "No menu available for this week"} mock_settings.assert_not_called() @patch("submit_order_handler.get_menu") def test_menu_rejects_malformed_week_without_lookup(mock_get_menu): event = _make_event( method="GET", path="/api/menu/not-a-week", path_parameters={"week": "not-a-week"}, ) status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 400 assert "current or YYYY-WNN" in body["error"] mock_get_menu.assert_not_called() # --------------------------------------------------------------------------- # Module-level patches that must be active before the handler is imported # --------------------------------------------------------------------------- # We patch boto3.client at the handler-module level so the module-level # `_lambda = boto3.client("lambda")` call gets a mock. # --------------------------------------------------------------------------- # We need to reset module-level caches between tests to avoid cross-test # leakage. The handler module caches settings and the Google client ID. @pytest.fixture(autouse=True) def _reset_handler_caches(): """Reset handler module-level caches before each test.""" submit_order_handler._settings = None submit_order_handler._settings_ts = 0.0 submit_order_handler._google_client_id = None submit_order_handler._google_client_id_ts = 0.0 yield @pytest.fixture(autouse=True) def _reset_shared_caches(): """Reset shared.secrets caches before each test.""" from shared import secrets secrets._secret_cache.clear() secrets._parameter_cache.clear() yield @pytest.fixture(autouse=True) def _mock_google_tokeninfo(): """Return a valid company identity unless a test overrides tokeninfo.""" mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": VALID_GOOGLE_CLIENT_ID, "hd": "seahavenind.com", "name": "Test User", "email": "test.user@seahavenind.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) with patch("submit_order_handler.urllib.request.urlopen", return_value=mock_resp): yield # =========================================================================== # PRICING PIPELINE (Critical) # =========================================================================== @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 50}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_discount_two_step_rounding( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Retail $10.25, 10% bulk, 50% subsidy. Step 1: bulk_price = 10.25 * 0.90 = 9.225 -> 9.23 (ROUND_HALF_UP) Step 2: emp_price = 9.23 * 0.50 = 4.615 -> 4.62 (ROUND_HALF_UP) Subtotal for qty=3: 4.62 * 3 = 13.86 """ from submit_order_handler import lambda_handler items = _make_items([(10.25, 3)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.25, 3)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] item = saved_order["items"][0] assert item["bulk_price"] == 9.23, ( f"bulk_price should be 9.23 (10.25 * 0.90 rounded HALF_UP), got {item['bulk_price']}" ) assert item["price"] == 4.62, ( f"emp_price should be 4.62 (9.23 * 0.50 rounded HALF_UP), got {item['price']}" ) assert item["subtotal"] == 13.86, ( f"subtotal should be 13.86 (4.62 * 3), got {item['subtotal']}" ) @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 50, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_discount_rounding_half_up_boundary( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Retail $10.05, 50% bulk, 0% subsidy. bulk_price = 10.05 * 0.50 = 5.025 -> 5.03 (ROUND_HALF_UP, not 5.02 banker's) """ from submit_order_handler import lambda_handler items = _make_items([(10.05, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.05, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] item = saved_order["items"][0] assert item["bulk_price"] == 5.03, ( f"bulk_price should be 5.03 (ROUND_HALF_UP for .025), got {item['bulk_price']}" ) assert item["price"] == 5.03, ( f"emp_price should equal bulk_price when subsidy is 0%, got {item['price']}" ) @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": -5, "company_subsidy_percent": 150}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_discount_clamping( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Negative bulk discount clamped to 0, subsidy >100 clamped to 100 (free).""" from submit_order_handler import lambda_handler items = _make_items([(20.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(20.00, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] item = saved_order["items"][0] assert item["bulk_price"] == 20.00, ( f"bulk_price should be 20.00 (bulk_discount clamped to 0%), got {item['bulk_price']}" ) assert item["price"] == 0.00, ( f"emp_price should be 0.00 (subsidy clamped to 100%), got {item['price']}" ) assert saved_order["total"] == 0.00, ( f"total should be 0.00 for free items, got {saved_order['total']}" ) @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_discount_both_zero( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """When both discounts are 0%, emp_price equals retail price.""" from submit_order_handler import lambda_handler items = _make_items([(15.99, 2)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(15.99, 2)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] item = saved_order["items"][0] assert item["retail_price"] == 15.99, ( f"retail_price mismatch: {item['retail_price']}" ) assert item["bulk_price"] == 15.99, ( f"bulk_price should equal retail when bulk discount is 0%, got {item['bulk_price']}" ) assert item["price"] == 15.99, ( f"emp_price should equal retail when both discounts are 0%, got {item['price']}" ) assert item["subtotal"] == 31.98, ( f"subtotal should be 15.99 * 2 = 31.98, got {item['subtotal']}" ) @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 25}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_total_summation_multiple_items( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Total is the sum of individually rounded subtotals, not a global multiply. Item A: retail=$10.00, bulk=10.00*0.90=9.00, emp=9.00*0.75=6.75, qty=2 -> subtotal=13.50 Item B: retail=$7.33, bulk=7.33*0.90=6.60, emp=6.60*0.75=4.95, qty=1 -> subtotal=4.95 Total = 13.50 + 4.95 = 18.45 """ from submit_order_handler import lambda_handler items = _make_items([(10.00, 2), (7.33, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 2), (7.33, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] item_a = saved_order["items"][0] assert item_a["subtotal"] == 13.50, ( f"Item A subtotal expected 13.50, got {item_a['subtotal']}" ) item_b = saved_order["items"][1] assert item_b["subtotal"] == 4.95, ( f"Item B subtotal expected 4.95, got {item_b['subtotal']}" ) assert saved_order["total"] == 18.45, ( f"total should be sum of rounded subtotals (13.50 + 4.95 = 18.45), got {saved_order['total']}" ) # =========================================================================== # AUTHENTICATION (Critical + High) # =========================================================================== @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch("submit_order_handler._get_google_client_id") @patch( "submit_order_handler._verify_portal_token", return_value={ "name": "Portal Employee", "email": "portal.employee@seahavenind.com", }, ) @patch("submit_order_handler.get_menu") def test_portal_token_identity_takes_precedence( mock_get_menu, mock_portal, mock_google_client_id, mock_settings, mock_week, mock_status, mock_put, mock_lambda, ): items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event( items, employee_name="Body Name", employee_email="body@example.com", extra_body={"google_id_token": "body-google-token"}, ) event["headers"] = {"authorization": "Bearer portal-id-token"} status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 200, body saved_order = mock_put.call_args.args[2] assert saved_order["employee_name"] == "Portal Employee" assert saved_order["employee_email"] == "portal.employee@seahavenind.com" mock_portal.assert_called_once_with("portal-id-token") mock_google_client_id.assert_not_called() @patch("submit_order_handler.put_order") @patch("submit_order_handler._get_google_client_id") @patch("submit_order_handler._verify_portal_token", return_value=None) def test_invalid_portal_token_never_falls_back_to_google_or_body( mock_portal, mock_google_client_id, mock_put ): event = _submit_event( _make_items([(10.00, 1)]), employee_name="Body Name", employee_email="body@seahavenind.com", extra_body={"google_id_token": "valid-google-token"}, ) event["headers"] = {"Authorization": "Bearer invalid-portal-token"} status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 403 assert body == {"error": "Invalid or unauthorized portal account"} mock_portal.assert_called_once_with("invalid-portal-token") mock_google_client_id.assert_not_called() mock_put.assert_not_called() @patch("submit_order_handler.put_order") @patch( "submit_order_handler._verify_portal_token", side_effect=submit_order_handler.CognitoVerificationUnavailable, ) def test_portal_verification_outage_returns_503(mock_portal, mock_put): event = _submit_event(_make_items([(10.00, 1)])) event["headers"] = {"authorization": "Bearer portal-id-token"} status, body = _parse_response(submit_order_handler.lambda_handler(event, None)) assert status == 503 assert body == {"error": "Authentication service temporarily unavailable"} mock_put.assert_not_called() @patch( "submit_order_handler.get_settings", return_value={"admin_emails": ["portal.admin@seahaven.com"]}, ) @patch( "submit_order_handler._verify_portal_token", return_value={"name": "Portal Admin", "email": "portal.admin@seahaven.com"}, ) @patch("submit_order_handler.looks_like_cognito_token", return_value=True) def test_in_handler_admin_check_accepts_portal_token( mock_looks_like, mock_portal, mock_settings ): event = _make_event( method="GET", path="/api/admin/orders", headers={"authorization": "Bearer portal-id-token"}, ) user, error = submit_order_handler._verify_admin(event) assert error is None assert user == {"name": "Portal Admin", "email": "portal.admin@seahaven.com"} @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch("submit_order_handler.get_parameter") @patch("submit_order_handler.get_menu") def test_missing_ssm_param_with_env_var_fails_closed( mock_get_menu, mock_get_parameter, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Env var set but SSM param missing -> 503 (fail closed, not silent manual fallback).""" from submit_order_handler import lambda_handler class ParameterNotFoundError(Exception): response = {"Error": {"Code": "ParameterNotFound"}} items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) mock_get_parameter.side_effect = ParameterNotFoundError("ParameterNotFound") with patch.dict( os.environ, {"GOOGLE_CLIENT_ID_PARAM": "/meal-order-manager/google-client-id"}, ): result = lambda_handler(_submit_event(items), None) status, body = _parse_response(result) assert status == 503, f"Expected 503 (fail closed), got {status}: {body}" mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler._google_auth_configured", return_value=True) def test_google_auth_required_when_configured( mock_gac, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """When Google auth is configured and no token is provided, return 403.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) # Body has name/email but no google_id_token event = _submit_event(items, google_token=None) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 403, f"Expected 403, got {status}: {body}" assert "Google authentication is required" in body["error"], ( f"Expected 'Google authentication is required' in error, got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler._google_auth_configured", return_value=True) def test_google_auth_bypass_prevention( mock_gac, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Google auth enabled + name/email in body but no token -> 403 (can't bypass).""" from submit_order_handler import lambda_handler body = { "employee_name": "Attacker Name", "employee_email": "attacker@seahavenind.com", "items": _make_items([(10.00, 1)]), # No google_id_token — trying to bypass with manual name/email } event = _make_event( method="POST", path="/submit", body=body, ) result = lambda_handler(event, None) status, body_resp = _parse_response(result) assert status == 403, f"Expected 403 (bypass prevented), got {status}: {body_resp}" mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.urllib.request.urlopen") @patch("submit_order_handler._google_auth_configured", return_value=True) def test_google_token_audience_mismatch( mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Token with wrong audience -> 403.""" from submit_order_handler import lambda_handler # Simulate Google returning token info with wrong audience mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": "wrong-client-id.apps.googleusercontent.com", "hd": "seahavenind.com", "name": "Test User", "email": "test@seahavenind.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_urlopen.return_value = mock_resp items = _make_items([(10.00, 1)]) event = _submit_event(items, extra_body={"google_id_token": "fake-token"}) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 403, f"Expected 403 for audience mismatch, got {status}: {body}" mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.urllib.request.urlopen") @patch("submit_order_handler._google_auth_configured", return_value=True) def test_google_token_domain_mismatch( mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Token with wrong hosted domain -> 403.""" from submit_order_handler import lambda_handler mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": VALID_GOOGLE_CLIENT_ID, "hd": "evil-corp.com", "name": "Evil User", "email": "evil@evil-corp.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_urlopen.return_value = mock_resp items = _make_items([(10.00, 1)]) event = _submit_event(items, extra_body={"google_id_token": "fake-token"}) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 403, f"Expected 403 for domain mismatch, got {status}: {body}" mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch( "submit_order_handler.urllib.request.urlopen", side_effect=urllib.error.URLError("Connection refused"), ) @patch("submit_order_handler._google_auth_configured", return_value=True) def test_google_token_service_unavailable( mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """URLError from Google tokeninfo -> 503.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items, extra_body={"google_id_token": "fake-token"}) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 503, f"Expected 503 for service unavailable, got {status}: {body}" assert "temporarily unavailable" in body["error"], ( f"Expected 'temporarily unavailable' in error, got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch( "submit_order_handler.urllib.request.urlopen", side_effect=urllib.error.HTTPError( "https://oauth2.googleapis.com/tokeninfo", 400, "Bad Request", {}, None, ), ) @patch("submit_order_handler._google_auth_configured", return_value=True) def test_google_token_http_error_returns_403( mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """HTTPError (e.g. 400 for expired token) -> 403, not 503.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items, extra_body={"google_id_token": "expired-token"}) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 403, ( f"Expected 403 for HTTP error (bad token), got {status}: {body}" ) assert "Invalid or unauthorized" in body["error"], ( f"Expected 'Invalid or unauthorized' in error, got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", side_effect=Exception("ParameterNotFound"), ) @patch("submit_order_handler._google_auth_configured", return_value=True) def test_ssm_failure_fails_closed( mock_gac, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """SSM fetch failure with auth configured -> 503, not silent fallback to manual.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 503, ( f"Expected 503 for SSM failure (fail-closed), got {status}: {body}" ) assert "temporarily unavailable" in body["error"], ( f"Expected 'temporarily unavailable' in error, got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.urllib.request.urlopen") @patch("submit_order_handler._google_auth_configured", return_value=True) @patch("submit_order_handler.get_menu") def test_google_token_valid_seahaven_com_domain( mock_get_menu, mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Google token with hd=seahaven.com (alternate domain) -> order saved.""" from submit_order_handler import lambda_handler mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": VALID_GOOGLE_CLIENT_ID, "hd": "seahaven.com", "name": "Test Employee", "email": "test@seahaven.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_urlopen.return_value = mock_resp items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event( items, extra_body={"google_id_token": "valid-token-seahaven"}, ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] assert saved_order["employee_email"] == "test@seahaven.com" @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.urllib.request.urlopen") @patch("submit_order_handler._google_auth_configured", return_value=True) @patch("submit_order_handler.get_menu") def test_google_token_valid_success( mock_get_menu, mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Valid Google token -> order saved with token's name/email.""" from submit_order_handler import lambda_handler mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": VALID_GOOGLE_CLIENT_ID, "hd": "seahavenind.com", "name": "Adam Moussa", "email": "adam.moussa@seahavenind.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_urlopen.return_value = mock_resp items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) # Body has different name/email — should be overridden by token event = _submit_event( items, employee_name="Wrong Name", employee_email="wrong@seahavenind.com", extra_body={"google_id_token": "valid-token-abc"}, ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] assert saved_order["employee_name"] == "Adam Moussa", ( f"Name should come from Google token, got: {saved_order['employee_name']}" ) assert saved_order["employee_email"] == "adam.moussa@seahavenind.com", ( f"Email should come from Google token, got: {saved_order['employee_email']}" ) @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_missing_google_client_id_fails_closed( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """No Google client ID configured -> 503, never manual fallback.""" from submit_order_handler import lambda_handler items = _make_items([(12.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(12.00, 1)]) event = _submit_event( items, employee_name="Manual User", employee_email="manual@seahavenind.com" ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 503, f"Expected fail-closed 503, got {status}: {body}" mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_submit_requires_no_api_key( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """A Google-authenticated submission succeeds without a shared API key.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200 without API key, got {status}: {body}" mock_put.assert_called_once() # =========================================================================== # SLUG GENERATION (Critical) # =========================================================================== @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch( "submit_order_handler._verify_google_token", return_value=( {"name": "Adam Moussa", "email": "Adam.Moussa@seahavenind.com"}, "ok", ), ) @patch("submit_order_handler.get_menu") def test_slug_from_email( mock_get_menu, mock_verify, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """'Adam.Moussa@seahavenind.com' -> slug 'adam.moussa@seahavenind.com'.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event( items, employee_name="Adam Moussa", employee_email="Adam.Moussa@seahavenind.com" ) result = lambda_handler(event, None) status, _ = _parse_response(result) assert status == 200, f"Expected 200, got {status}" # put_order is called with (week, slug, order_data) slug = mock_put.call_args[0][1] assert slug == "adam.moussa@seahavenind.com", ( f"Slug should be 'adam.moussa@seahavenind.com', got '{slug}'" ) @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler._verify_google_token") @patch("submit_order_handler.get_menu") def test_slug_edge_cases( mock_get_menu, mock_verify, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Slug uses the full lowercase Google email across allowed domains.""" from submit_order_handler import lambda_handler mock_verify.side_effect = [ ( { "name": "First Middle Last", "email": "First.Middle.Last@seahavenind.com", }, "ok", ), ({"name": "Bob Smith", "email": "Bob@seahaven.com"}, "ok"), ] # Test 1: full email preserved items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event( items, employee_name="First Middle Last", employee_email="ignored@seahavenind.com", ) lambda_handler(event, None) slug_1 = mock_put.call_args[0][1] assert slug_1 == "first.middle.last@seahavenind.com", ( f"Slug should be full lowercase email, got '{slug_1}'" ) # Test 2: different domains produce different slugs (no collision) mock_put.reset_mock() mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event( items, employee_name="Ignored", employee_email="ignored@seahavenind.com" ) lambda_handler(event, None) slug_2 = mock_put.call_args[0][1] assert slug_2 == "bob@seahaven.com", ( f"Slug should be full lowercase email, got '{slug_2}'" ) assert slug_1 != slug_2, "Different emails must produce different slugs" # =========================================================================== # FORM STATUS (Critical + High) # =========================================================================== @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") def test_form_status_open(mock_week, mock_status): """Status 'open' — response has week and status, no reopen_at.""" from submit_order_handler import lambda_handler event = _make_event( method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"} ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" assert body["status"] == "open", f"Expected status='open', got '{body['status']}'" assert body["week"] == "2026-W20", f"Expected week='2026-W20', got '{body['week']}'" assert "reopen_at" not in body, "reopen_at should NOT be present when form is open" @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") def test_form_status_current_maps_to_current_week(mock_week, mock_status): from submit_order_handler import lambda_handler event = _make_event( method="GET", path="/form-status/current", path_parameters={"week": "current"} ) status, body = _parse_response(lambda_handler(event, None)) assert status == 200, f"Expected 200, got {status}: {body}" assert body["week"] == "2026-W20" mock_status.assert_called_once_with("2026-W20") @patch("submit_order_handler.get_form_status") def test_form_status_rejects_invalid_week(mock_status): from submit_order_handler import lambda_handler event = _make_event( method="GET", path="/form-status/nope", path_parameters={"week": "nope"} ) status, body = _parse_response(lambda_handler(event, None)) assert status == 400, f"Expected 400, got {status}: {body}" assert "week path param" in body["error"] mock_status.assert_not_called() @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2026-W20") def test_form_status_closed_reopen_at(mock_week, mock_status): """Closed on Thursday -> reopen_at is next Monday 8am Eastern.""" from submit_order_handler import lambda_handler, EASTERN # Freeze "now" to Thursday 2026-05-14 at 10:00 AM Eastern thursday = datetime(2026, 5, 14, 10, 0, 0, tzinfo=EASTERN) with patch("submit_order_handler._eastern_now", return_value=thursday): event = _make_event( method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"}, ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" assert body["status"] == "closed", ( f"Expected status='closed', got '{body['status']}'" ) assert "reopen_at" in body, "reopen_at should be present when form is closed" # Next Monday from Thursday 2026-05-14 is Monday 2026-05-18 expected_monday = datetime(2026, 5, 18, 8, 0, 0, tzinfo=EASTERN) expected_ts = int(expected_monday.timestamp()) assert body["reopen_at"] == expected_ts, ( f"reopen_at should be {expected_ts} (Mon 2026-05-18 8am ET), got {body['reopen_at']}" ) @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2026-W20") def test_form_status_monday_before_8am(mock_week, mock_status): """Monday before 8am -> reopen_at is TODAY at 8am, not next Monday.""" from submit_order_handler import lambda_handler, EASTERN # Monday 2026-05-18 at 6:30 AM Eastern (before 8am cutoff) monday_early = datetime(2026, 5, 18, 6, 30, 0, tzinfo=EASTERN) with patch("submit_order_handler._eastern_now", return_value=monday_early): event = _make_event( method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"}, ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" assert "reopen_at" in body, "reopen_at should be present when form is closed" # days_until_monday = (7 - 0) % 7 = 0, and hour < 8, so days_until_monday stays 0 # -> reopen_at is TODAY (same Monday) at 8am expected_today = datetime(2026, 5, 18, 8, 0, 0, tzinfo=EASTERN) expected_ts = int(expected_today.timestamp()) assert body["reopen_at"] == expected_ts, ( f"reopen_at should be {expected_ts} (today Mon 2026-05-18 8am ET), got {body['reopen_at']}" ) @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2025-W45") def test_form_status_closed_saturday_before_dst_end(mock_week, mock_status): """Saturday before fall-back: reopen_at is the *calendar* Monday at 8am ET. Using timedelta(days=n) for n weekdays is 24n hours and can land on the wrong local calendar day when a 25-hour Sunday sits in between. """ from submit_order_handler import lambda_handler, EASTERN # Nov 1 2025 is Saturday (DST still on until early Nov 2). Next Monday is Nov 3. saturday = datetime(2025, 11, 1, 12, 0, 0, tzinfo=EASTERN) with patch("submit_order_handler._eastern_now", return_value=saturday): event = _make_event( method="GET", path="/form-status/2025-W45", path_parameters={"week": "2025-W45"}, ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200 expected_monday = datetime(2025, 11, 3, 8, 0, 0, tzinfo=EASTERN) expected_ts = int(expected_monday.timestamp()) assert body["reopen_at"] == expected_ts, ( f"reopen_at should be {expected_ts} (Mon 2025-11-03 8am ET), got {body['reopen_at']}" ) # =========================================================================== # VALIDATION (High) # =========================================================================== @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch( "submit_order_handler._verify_google_token", return_value=({"name": "", "email": "test@seahavenind.com"}, "ok"), ) def test_submit_missing_name( mock_verify, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """A Google identity without a name -> 400.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event( items, employee_name="", employee_email="test@seahavenind.com" ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 400, f"Expected 400 for missing name, got {status}: {body}" assert "name" in body["error"].lower(), ( f"Error should mention name, got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch( "submit_order_handler._verify_google_token", return_value=({"name": "Test User", "email": ""}, "ok"), ) def test_submit_missing_email( mock_verify, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """A Google identity without an email -> 400.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items, employee_name="Test User", employee_email="") result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 400, f"Expected 400 for missing email, got {status}: {body}" assert "email" in body["error"].lower(), ( f"Error should mention email, got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) def test_submit_zero_quantity_only( mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """All items with quantity 0 -> 400.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 0), (15.00, 0)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 400, f"Expected 400 for zero-quantity items, got {status}: {body}" assert "at least one meal" in body["error"].lower(), ( f"Error should mention 'at least one meal', got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) def test_submit_form_closed( mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """Form closed -> 410.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 410, f"Expected 410 for closed form, got {status}: {body}" assert "closed" in body["error"].lower(), ( f"Error should mention 'closed', got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={ "bulk_discount_percent": 0, "company_subsidy_percent": 0, "admin_emails": ["adam@seahavenind.com"], }, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.urllib.request.urlopen") @patch("submit_order_handler._google_auth_configured", return_value=True) @patch("submit_order_handler.get_menu") def test_admin_can_submit_when_form_closed( mock_get_menu, mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Admin user can submit even when form is closed.""" from submit_order_handler import lambda_handler mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": VALID_GOOGLE_CLIENT_ID, "hd": "seahavenind.com", "name": "Adam Moussa", "email": "adam@seahavenind.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_urlopen.return_value = mock_resp items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event(items, extra_body={"google_id_token": "admin-token"}) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Admin should bypass closed form, got {status}: {body}" mock_put.assert_called_once() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={ "bulk_discount_percent": 0, "company_subsidy_percent": 0, "admin_emails": ["adam@seahavenind.com"], }, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.urllib.request.urlopen") @patch("submit_order_handler._google_auth_configured", return_value=True) @patch("submit_order_handler.get_menu") def test_non_admin_blocked_when_form_closed( mock_get_menu, mock_gac, mock_urlopen, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Non-admin user still blocked when form is closed.""" from submit_order_handler import lambda_handler mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( { "aud": VALID_GOOGLE_CLIENT_ID, "hd": "seahavenind.com", "name": "Regular Employee", "email": "employee@seahavenind.com", } ).encode() mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_urlopen.return_value = mock_resp items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event(items, extra_body={"google_id_token": "user-token"}) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 410, f"Non-admin should be blocked, got {status}: {body}" mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="not_found") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) def test_submit_no_menu( mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """No menu available -> 404.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 404, f"Expected 404 for missing menu, got {status}: {body}" assert "no menu" in body["error"].lower(), ( f"Error should mention 'no menu', got: {body['error']}" ) mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_unknown_meal_returns_400( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Line items must match menu meal names (reject client-injected SKUs).""" from submit_order_handler import lambda_handler mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event( [{"name": "Totally Fake Meal", "retail_price": 1.0, "quantity": 1}] ) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 400, f"Expected 400, got {status}: {body}" assert "not on this week's menu" in body["error"].lower(), body["error"] mock_put.assert_not_called() @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_retail_price_from_menu_not_request_body( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Billing uses Dynamo menu retail, not client-supplied retail_price.""" from submit_order_handler import lambda_handler mock_get_menu.return_value = {"meals": [{"name": "Meal 1", "price": 100.0}]} items = [{"name": "Meal 1", "retail_price": 0.01, "quantity": 1}] event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200, got {status}: {body}" saved = mock_put.call_args[0][2] assert saved["items"][0]["retail_price"] == 100.0, saved["items"][0] assert saved["total"] == 100.0, saved["total"] @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_menu_missing_priced_meals_returns_503( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): from submit_order_handler import lambda_handler mock_get_menu.return_value = {"meals": []} items = _make_items([(10.00, 1)]) result = lambda_handler(_submit_event(items), None) status, body = _parse_response(result) assert status == 503, f"Expected 503, got {status}: {body}" assert "unavailable" in body["error"].lower(), body["error"] mock_put.assert_not_called() # =========================================================================== # RELIABILITY (High) # =========================================================================== @patch("submit_order_handler._dispatch_job") @patch("submit_order_handler.put_order") @patch("submit_order_handler.get_form_status", return_value="open") @patch("submit_order_handler.current_week", return_value="2026-W20") @patch( "submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0}, ) @patch( "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID ) @patch("submit_order_handler.get_menu") def test_slack_failure_does_not_fail_order( mock_get_menu, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Lambda invoke for Slack notification raises, order still saved, returns 200.""" from submit_order_handler import lambda_handler mock_lam.invoke.side_effect = Exception("Lambda invoke failed: connection timeout") items = _make_items([(10.00, 1)]) mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)]) event = _submit_event(items) result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 200, f"Expected 200 despite Slack failure, got {status}: {body}" assert body["status"] == "ok", f"Expected status='ok', got '{body['status']}'" mock_put.assert_called_once() # --------------------------------------------------------------------------- # Admin summary-PDF endpoint # --------------------------------------------------------------------------- def _pdf_event(week=None): qs = {"week": week} if week else {} return { "rawPath": "/api/admin/summary-pdf", "requestContext": {"http": {"method": "GET"}}, "queryStringParameters": qs, "headers": {"authorization": "Bearer admin-token"}, } @patch.dict(os.environ, {"REPORTS_BUCKET": "test-reports-bucket"}) @patch("submit_order_handler._s3") @patch("submit_order_handler.get_summary") @patch( "submit_order_handler._verify_admin", return_value=({"email": "adam@seahavenind.com"}, None), ) def test_admin_summary_pdf_returns_presigned_url( mock_verify, mock_get_summary, mock_s3 ): """Closed week with a stamped PDF key returns a presigned URL.""" from submit_order_handler import lambda_handler mock_get_summary.return_value = { "weekly_summary_pdf_s3_key": "reports/2026-W22/weekly-summary-2026-W22.pdf" } mock_s3.generate_presigned_url.return_value = "https://signed.example/pdf" status, body = _parse_response(lambda_handler(_pdf_event("2026-W22"), None)) assert status == 200, f"Expected 200, got {status}: {body}" assert body["url"] == "https://signed.example/pdf" assert body["week"] == "2026-W22" mock_s3.generate_presigned_url.assert_called_once_with( "get_object", Params={ "Bucket": "test-reports-bucket", "Key": "reports/2026-W22/weekly-summary-2026-W22.pdf", }, ExpiresIn=300, ) @patch("submit_order_handler.get_summary", return_value=None) @patch( "submit_order_handler._verify_admin", return_value=({"email": "adam@seahavenind.com"}, None), ) def test_admin_summary_pdf_404_when_week_not_closed(mock_verify, mock_get_summary): """No SUMMARY item (week still open) returns 404.""" from submit_order_handler import lambda_handler status, body = _parse_response(lambda_handler(_pdf_event("2026-W23"), None)) assert status == 404, f"Expected 404, got {status}: {body}" assert "no summary pdf" in body["error"].lower() @patch( "submit_order_handler._verify_admin", return_value=({"email": "adam@seahavenind.com"}, None), ) def test_admin_summary_pdf_400_without_week(mock_verify): """Missing week query param returns 400.""" from submit_order_handler import lambda_handler status, body = _parse_response(lambda_handler(_pdf_event(), None)) assert status == 400, f"Expected 400, got {status}: {body}" @patch( "submit_order_handler._verify_admin", return_value=({"email": "adam@seahavenind.com"}, None), ) def test_admin_summary_pdf_400_malformed_week(mock_verify): """Week not matching YYYY-WNN returns 400 before any lookup.""" from submit_order_handler import lambda_handler status, body = _parse_response(lambda_handler(_pdf_event("../../etc"), None)) assert status == 400, f"Expected 400, got {status}: {body}" @patch("submit_order_handler._s3") @patch("submit_order_handler.get_summary") @patch( "submit_order_handler._verify_admin", return_value=({"email": "adam@seahavenind.com"}, None), ) def test_admin_summary_pdf_rejects_tampered_key(mock_verify, mock_get_summary, mock_s3): """A PDF key outside the expected shape is never presigned (500, no URL).""" from submit_order_handler import lambda_handler mock_get_summary.return_value = { "weekly_summary_pdf_s3_key": "reports/2026-W22/payroll-deductions.csv" } status, body = _parse_response(lambda_handler(_pdf_event("2026-W22"), None)) assert status == 500, f"Expected 500 for tampered key, got {status}: {body}" mock_s3.generate_presigned_url.assert_not_called() @patch( "submit_order_handler._verify_admin", return_value=(None, submit_order_handler.response(401, {"error": "Missing token"})), ) def test_admin_summary_pdf_requires_admin(mock_verify): """Auth failure from _verify_admin is returned as-is.""" from submit_order_handler import lambda_handler status, body = _parse_response(lambda_handler(_pdf_event("2026-W22"), None)) assert status == 401, f"Expected 401, got {status}: {body}" # =========================================================================== # GET /api/orders/{week} (caller-only) # =========================================================================== def _orders_event(week="2026-W36", token="portal-id-token"): headers = {"authorization": f"Bearer {token}"} if token else {} return _make_event( method="GET", path=f"/api/orders/{week}", headers=headers, path_parameters={"week": week}, ) @patch( "submit_order_handler.get_order", return_value={ "employee_email": "portal.employee@seahavenind.com", "employee_name": "Portal Employee", "items": [{"name": "Soup", "quantity": 1}], "total": 5.5, }, ) @patch( "submit_order_handler._verify_portal_token", return_value={ "name": "Portal Employee", "email": "portal.employee@seahavenind.com", }, ) @patch("submit_order_handler.looks_like_cognito_token", return_value=True) def test_my_orders_returns_only_caller_email(mock_looks_like, mock_portal, mock_get): status, body = _parse_response( submit_order_handler.lambda_handler(_orders_event(), None) ) assert status == 200 assert body == { "week": "2026-W36", "orders": [{"employee_email": "portal.employee@seahavenind.com"}], } mock_get.assert_called_once_with("2026-W36", "portal.employee@seahavenind.com") @patch("submit_order_handler.get_order", return_value=None) @patch( "submit_order_handler._verify_portal_token", return_value={ "name": "Portal Employee", "email": "portal.employee@seahavenind.com", }, ) @patch("submit_order_handler.looks_like_cognito_token", return_value=True) def test_my_orders_empty_when_none(mock_looks_like, mock_portal, mock_get): status, body = _parse_response( submit_order_handler.lambda_handler(_orders_event(), None) ) assert status == 200 assert body == {"week": "2026-W36", "orders": []} @patch("submit_order_handler.get_order") @patch( "submit_order_handler._verify_portal_token", return_value={ "name": "Portal Employee", "email": "portal.employee@seahavenind.com", }, ) @patch("submit_order_handler.looks_like_cognito_token", return_value=True) def test_my_orders_rejects_current_week(mock_looks_like, mock_portal, mock_get): status, body = _parse_response( submit_order_handler.lambda_handler(_orders_event(week="current"), None) ) assert status == 400 assert "YYYY-WNN" in body["error"] mock_get.assert_not_called() def test_my_orders_requires_bearer(): status, body = _parse_response( submit_order_handler.lambda_handler(_orders_event(token=""), None) ) assert status == 403 assert body == {"error": "Authentication required"} @patch("submit_order_handler.get_order") @patch( "submit_order_handler._verify_portal_token", return_value={ "name": "Portal Employee", "email": "portal.employee@seahavenind.com", }, ) @patch("submit_order_handler.looks_like_cognito_token", return_value=True) def test_my_orders_rejects_malformed_week(mock_looks_like, mock_portal, mock_get): status, body = _parse_response( submit_order_handler.lambda_handler( _orders_event(week="not-a-week"), None, ) ) assert status == 400 mock_get.assert_not_called() assert "YYYY-WNN" in body["error"]