Skip to content

Single fake transactional endpoint for e2e tests (remove capture, mock inbox, real inbox) #64

Description

@alexeygrigorev

Goal

Datamailer currently has three overlapping "don't really send" mechanisms used
for testing. They do essentially the same job in three different ways, each with its
own settings, endpoints, storage, and quirks. This is confusing and hard to maintain.

Replace all of them with one fake, synchronous, drop-in transactional endpoint
built for e2e tests: you POST the same payload you'd POST to the real send endpoint,
and you get the rendered email back inline in a single response — no real
delivery, no queue, no separate inbox to poll.

Why (the current mess)

Mechanism How it works Config Read path
Capture / testbed DATAMAILER_DELIVERY_MODE=capture makes the worker store a CapturedEmail row instead of calling SES DATAMAILER_DELIVERY_MODE, DATAMAILER_CAPTURE_UI GET /api/testbed/runs, .../runs/{id}, .../runs/{id}/messages/{id} (disabled on deployed: capture_api_disabled)
Mock inbox Send to a mock address (*@mailbox.test or +e2e); worker marks it sent as mock-inbox:{id} and skips SES MOCK_INBOX_ENABLED, MOCK_INBOX_DOMAIN, MOCK_INBOX_PLUS_TAG GET /api/mock-inbox/messages?address=..., .../messages/{id}
Real inbox Send to *@mailer.dtcdev.click; goes through real SES, received back from an inbound S3 bucket REAL_INBOX_ENABLED, REAL_INBOX_DOMAIN, REAL_INBOX_S3_BUCKET, REAL_INBOX_S3_PREFIX, REAL_INBOX_MAX_SCAN_OBJECTS GET /api/inbox/messages, .../messages/{key}

All three require the send-then-poll dance: submit, then hit a second endpoint to
read what was rendered. That's slow and awkward in e2e tests.

Proposed endpoint

POST /api/transactional/test-send (final name TBD — see open decisions)

  • Auth: same client Bearer token as the real endpoint.

  • Request body: byte-for-byte identical to POST /api/transactional/send
    (email, template_key, context, metadata, from_email, reply_to, cc,
    bcc, headers, message_parts, category_tag, idempotency_key). Reuses the
    exact same validation (validate_transactional_send_payload).

  • Behavior: resolves the template + sender, renders subject / html / text with the
    provided context, evaluates the delivery decision (suppression / category
    preferences) — but never enqueues, never calls SES, and persists nothing.

  • Response: 200 with the rendered email inline, e.g.

    {
      "rendered": {
        "to": "alexey+e2e@datatalks.club",
        "from_email": "...",
        "from_email_address": "...",
        "reply_to": "", "cc": [], "bcc": [], "headers": {},
        "subject": "Welcome to AI Dev Tools",
        "html_body": "<...rendered...>",
        "text_body": "...rendered..."
      },
      "template_key": "registration-confirmation",
      "would_deliver": true,
      "delivery_decision": { "allowed": true, "reason": "" }
    }

    would_deliver / delivery_decision let e2e tests assert suppression and category
    logic without a second request. Rendering itself always happens regardless of the
    decision, so you can inspect the output even for suppressed contacts.

This is the whole test surface: one request in, rendered email out, nothing sent.

Remove (consolidation)

Delete these entirely once the new endpoint replaces them:

Capture / testbed

  • mailing/services/capture.py
  • CapturedEmail model (mailing/models.py) + a migration dropping the captured_emails table
  • Views: api_testbed_runs, api_testbed_run_detail, api_testbed_run_message, _capture_api_disabled_response
  • URLs: the three api/testbed/... routes
  • Settings: DATAMAILER_DELIVERY_MODE, DATAMAILER_CAPTURE_UI
  • Capture branches in transactional_sender.py (capture_mode_enabled / _mark_captured) and campaign_sender.py (see wrinkle below)
  • mailing/tests/test_capture_mode.py, api_docs entries, docs/api.md capture section

Mock inbox

  • mailing/services/mock_inbox.py
  • Views: api_mock_inbox_messages, api_mock_inbox_message_detail, _mock_inbox_disabled_response
  • URLs: the two api/mock-inbox/... routes
  • Settings: MOCK_INBOX_ENABLED, MOCK_INBOX_DOMAIN, MOCK_INBOX_PLUS_TAG
  • Mock branch in transactional_sender.py (is_mock_address)
  • mailing/tests/test_mock_inbox.py, docs/api.md mock-inbox section

Real inbox

  • mailing/services/real_inbox.py
  • Views: api_real_inbox_messages, api_real_inbox_message_detail
  • URLs: the two api/inbox/... routes
  • Settings: all REAL_INBOX_*
  • Real-inbox branch in transactional_sender.py (is_real_inbox_address)
  • mailing/tests/test_real_inbox.py

Net effect: transactional_sender.py and campaign_sender.py lose all their
test-mode branches and only ever do the real SES path.

e2e usage

Replace the current send-then-poll pattern:

POST /api/transactional/send  (to alexey+e2e@datatalks.club)
GET  /api/mock-inbox/messages?address=alexey+e2e@datatalks.club   # poll

with a single call:

POST /api/transactional/test-send   ->   assert rendered.subject / rendered.html_body inline

No special addresses, no global env mode, no second request.

Acceptance criteria

  • POST /api/transactional/test-send accepts the identical payload/validation as /api/transactional/send and returns rendered subject/html_body/text_body inline.
  • It never calls SES, never enqueues to SQS, and creates no DB rows.
  • It reports the delivery decision (suppression / category) without blocking rendering.
  • Capture, mock-inbox, and real-inbox code/endpoints/settings/tests are fully removed; the app boots and the full test suite passes.
  • A migration drops the captured_emails table.
  • docs/api.md and the api-docs registry document only the new endpoint.

Open decisions

  1. Endpoint name: test-send vs preview vs render vs fake.
  2. Campaign wrinkle: capture mode is also the e2e path for campaign sends
    (campaign_sender.py). Removing capture removes campaign e2e coverage. Options:
    (a) accept the loss, (b) add a parallel POST /api/campaigns/{key}/test-send
    render endpoint. Note there is already an api_campaign_preview / api_campaign_test_send
    pair — clarify how those relate.
  3. Real-inbox removal: it's the only mechanism that exercises the real SES
    send + inbound-receive path, so removing it drops true end-to-end delivery
    coverage. Confirm that's acceptable (the new fake never touches SES).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions