From 96d59247cdbbd7bbc12c5ee4ea5c43994f964c9f Mon Sep 17 00:00:00 2001 From: Cam Date: Wed, 10 Jun 2026 22:48:36 -0500 Subject: [PATCH] Initial tests for alta_open_lambda With the upcoming Neon webhook deprecation, we need for the alta-open-update lambda to be able to handle both the legacy and the new webhook format. Roughly, this is what the deprecation strategy looks like: * Enable new Neon webhooks (already done) * Update lambda to work on both styles of payload (new or legacy) * Monitor results, ensure that there are no errors and that the new webhooks have the correct behavior. * Disable legacy webhooks (one at a time) in Neon. * Monitor results, ensure that operations are happening as expected (accounts are actually being created, etc.) * Update lambda to throw an error for legacy webhook calls, without updating Alta. * (Eventually) Remove code for handling legacy Neon webhooks. To help with the first step of updating the lambda, I want to write a test suite to capture the current behavior. The point of this isn't to catch any new bugs, just to verify what's already happening in the lambda. Once we can show what behavior is currently happening under the legacy webhooks, we can start adding in the new webhook formats and ensuring that the tests work with those, too, using the `legacy` flag if needed. --- tests/test_alta_open_lambda.py | 87 ++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_alta_open_lambda.py diff --git a/tests/test_alta_open_lambda.py b/tests/test_alta_open_lambda.py new file mode 100644 index 0000000..fd86d8c --- /dev/null +++ b/tests/test_alta_open_lambda.py @@ -0,0 +1,87 @@ +""" +Characterization tests for the alta_open_lambda webhook handler. + +These tests will pin down exactly what ``lambda_handler`` does TODAY, before the +planned refactor that unifies handling of the two Neon webhook payload formats. +The suite is green against the pre-refactor handler; as the refactor converges +the formats, the relevant assertions get flipped one at a time. + +Incoming event formats: +------------------------------------------------------------ +legacy (old_webhooks.json / events.json): + - integer account IDs (e.g. 9058) + - nested shapes (membershipEnrollment + transaction; tickets as + ``{"ticket": [...]}``); + - customParameters may be null or tagged ``legacy: "true"`` + +new (new_webhooks.json, post 2026-05-09): + - string account IDs (e.g. "1877") + - flat shapes (membership fields hoisted to the data top level; tickets as + plain arrays); + - customParameters tagged ``legacy: "false"`` +""" + +import json +import sys +from pathlib import Path + +import pytest + +# lambda_function lives in alta_open_lambda/, which is not a package +sys.path.insert(0, str(Path(__file__).parent.parent / "alta_open_lambda")) + +import lambda_function as lf + + +@pytest.fixture +def openpath(mocker): + """Mock the OpenPath update so we only exercise webhook parsing.""" + return mocker.patch.object(lf, "openPathUpdateSingle") + + +def make_event(event_trigger, data, custom_parameters): + """Wrap a webhook body in the Lambda Function URL envelope. Production + always delivers body as a JSON string.""" + return { + "body": json.dumps( + { + "eventTrigger": event_trigger, + "eventTimestamp": "2026-06-10T12:00:00.000-05:00", + "organizationId": "asmbly", + "data": data, + "customParameters": custom_parameters, + } + ) + } + + +# customParameters variants seen in production +UNKNOWN_PARAMS = None # truly-legacy events send customParameters: null +LEGACY_PARAMS = {"webhook_name": "UpdateMembershipLegacy", "legacy": "true"} +NEW_PARAMS = {"webhook_name": "UpdateMembership20260509", "legacy": "false"} + + +# =========================================================================== +# Guard clauses -- trigger-independent; these exercise the harness end to end +# =========================================================================== + + +def test_ignores_event_without_body(openpath): + lf.lambda_handler({"requestContext": {}}, {}) + openpath.assert_not_called() + + +def test_ignores_unknown_trigger(openpath): + lf.lambda_handler(make_event("somethingElse", {"accountId": 1}, NEW_PARAMS), {}) + openpath.assert_not_called() + + +def test_body_as_dict_currently_raises(openpath): + """Production delivers body as a JSON string; the handler always + json.loads(body). A dict body (as some saved fixtures store it) raises + TypeError today. Pinned so the refactor can decide whether to accept it.""" + event = make_event("editAccount", {"individualAccount": {"accountId": 1}}, NEW_PARAMS) + event["body"] = json.loads(event["body"]) + with pytest.raises(TypeError): + lf.lambda_handler(event, {}) + openpath.assert_not_called()