"""Unit tests for functions/submit_order/handler.py All external dependencies (DynamoDB, SSM, Secrets Manager, Lambda invoke) are mocked — no real AWS calls are made. """ import json import os import sys import urllib.error from datetime import datetime 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("FORM_API_KEY_SECRET", "test/form-api-key") 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, "functions", "submit_order", "handler.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") TEST_API_KEY = "test-api-key-12345" VALID_GOOGLE_CLIENT_ID = "123456789.apps.googleusercontent.com" 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, api_key=TEST_API_KEY, employee_name="Test User", employee_email="test.user@seahavenind.com", extra_body=None, ): """Shortcut for a typical POST /submit event with items.""" body = { "employee_name": employee_name, "employee_email": employee_email, "items": items, } if extra_body: body.update(extra_body) return _make_event( method="POST", path="/submit", body=body, headers={"x-api-key": api_key}, ) 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) ] } # --------------------------------------------------------------------------- # 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 _api_key, _settings, _google_client_id. @pytest.fixture(autouse=True) def _reset_handler_caches(): """Reset handler module-level caches before each test.""" submit_order_handler._api_key = None 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 # =========================================================================== # PRICING PIPELINE (Critical) # =========================================================================== @patch("submit_order_handler._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_discount_two_step_rounding( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_discount_rounding_half_up_boundary( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_discount_clamping( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_discount_both_zero( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_total_summation_multiple_items( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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) 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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, headers={"x-api-key": TEST_API_KEY}, ) 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @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_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_manual_fallback_when_google_not_configured( mock_get_menu, mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """No Google client ID configured -> manual name/email accepted and order saved.""" 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 == 200, f"Expected 200, got {status}: {body}" saved_order = mock_put.call_args[0][2] assert saved_order["employee_name"] == "Manual User", ( f"Name should be from manual input, got: {saved_order['employee_name']}" ) assert saved_order["employee_email"] == "manual@seahavenind.com", ( f"Email should be from manual input, got: {saved_order['employee_email']}" ) @patch("submit_order_handler._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") def test_submit_invalid_api_key( mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """Wrong x-api-key -> 403.""" from submit_order_handler import lambda_handler items = _make_items([(10.00, 1)]) event = _submit_event(items, api_key="wrong-api-key") result = lambda_handler(event, None) status, body = _parse_response(result) assert status == 403, f"Expected 403 for invalid API key, got {status}: {body}" assert "Invalid API key" in body["error"], ( f"Expected 'Invalid API key' in error, got: {body['error']}" ) mock_put.assert_not_called() # =========================================================================== # SLUG GENERATION (Critical) # =========================================================================== @patch("submit_order_handler._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_slug_from_email( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_slug_edge_cases( mock_get_menu, mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam, ): """Slug uses full lowercase email, preserving uniqueness across domains.""" from submit_order_handler import lambda_handler # 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="First.Middle.Last@x.com", ) lambda_handler(event, None) slug_1 = mock_put.call_args[0][1] assert slug_1 == "first.middle.last@x.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="Bob Smith", employee_email="Bob@other.com" ) lambda_handler(event, None) slug_2 = mock_put.call_args[0][1] assert slug_2 == "bob@other.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="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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") def test_submit_missing_name( mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """Empty employee 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") def test_submit_missing_email( mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam ): """Empty employee 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") def test_submit_zero_quantity_only( mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") def test_submit_form_closed( mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") def test_submit_no_menu( mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_unknown_meal_returns_400( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_retail_price_from_menu_not_request_body( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_menu_missing_priced_meals_returns_503( mock_get_menu, mock_gcid, mock_secret, 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._lambda") @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_secret", return_value=TEST_API_KEY) @patch("submit_order_handler._get_google_client_id", return_value="") @patch("submit_order_handler.get_menu") def test_slack_failure_does_not_fail_order( mock_get_menu, mock_gcid, mock_secret, 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()