Skip to content

Add E2E test infrastructure and purchase flow tests#10781

Merged
psincraian merged 11 commits intomainfrom
worktree-pure-soaring-eclipse
Apr 7, 2026
Merged

Add E2E test infrastructure and purchase flow tests#10781
psincraian merged 11 commits intomainfrom
worktree-pure-soaring-eclipse

Conversation

@psincraian
Copy link
Copy Markdown
Contributor

Summary

  • Introduce an in-process E2E testing framework for backend purchase flows with a TaskDrain utility that reads Dramatiq messages from Redis and executes task functions inline
  • Add 12 E2E tests covering one-time purchases, subscriptions, post-purchase flows, and subscription lifecycle events
  • Organize tests by lifecycle phase: purchase/, post_purchase/, lifecycle/

Infrastructure (tests/e2e/infra/)

  • TaskDrain: flushes JobQueueManager to Redis, drains all queues by executing task functions with proper CurrentMessage context. Auto-discovers task modules via rglob. Raises TaskDrainError with chained exception on failure.
  • StripeSimulator: configures mock return values and generates matching webhook charge objects with consistent IDs. expect_payment() / send_charge_webhook() API.
  • EmailCapture: intercepts emails for assertion via mock sender.
  • External HTTP guardrail: blocks unmocked outbound calls with actionable error message.
  • Autouse mocks for Stripe, tax calculation, PostHog, Loops, webhooks, checkout events.

Tests

Phase Test What it verifies
purchase/one_time test_fixed_price $25 product -> order + email
purchase/one_time test_free_product Free -> no Stripe, handle_free_success
purchase/one_time test_with_discount 20% off $50 -> $40 order
purchase/one_time test_with_tax 10% exclusive tax -> $5 on order
purchase/one_time test_payment_failure charge.failed -> no order, checkout reopens
purchase/subscription test_fixed_price $15/month -> subscription active + order
post_purchase test_seat_claim 3 seats -> assign -> claim -> benefits
post_purchase test_benefit_grants 4 benefit types: custom, license_key, meter_credit, feature_flag
lifecycle test_renewal Active sub -> cycle task -> renewal order

Test plan

  • All 12 E2E tests pass locally (12 passed in ~12s)
  • Backend lint passes
  • Backend type check passes
  • Verify tests pass in CI with Docker (Postgres + Redis)

Generated with Claude Code

psincraian and others added 10 commits April 2, 2026 14:03
Introduce an in-process E2E testing framework for backend purchase flows.
The infrastructure mounts the FastAPI app via httpx ASGI transport, drains
Dramatiq background tasks inline via a TaskDrain utility, and mocks
external services (Stripe, email, tax, PostHog, Loops, webhooks).

Key components:
- TaskDrain: reads Dramatiq messages from Redis, executes task functions
  inline with proper JobQueueManager context for sub-task chaining
- StripeSimulator: configures mock return values and generates matching
  webhook charge objects with consistent IDs
- EmailCapture: intercepts emails for assertion
- External HTTP guardrail: blocks unmocked outbound calls

Includes a working one-time purchase E2E test covering:
checkout creation → payment confirmation → Stripe webhook →
order verification → email delivery

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorganize tests/e2e/ into a phase-based folder structure:
- infra/       — test infrastructure (TaskDrain, StripeSimulator, mocks)
- purchase/    — buying something (one_time/, subscription/)
- post_purchase/ — actions after order exists (seat claims)
- lifecycle/   — ongoing subscription events (renewal)

Add new purchase tests:
- one_time/test_free_product.py — free product, no Stripe payment
- one_time/test_with_discount.py — 20% discount applied to order
- subscription/test_happy_path.py — monthly plan → subscription active

Infrastructure improvements:
- Auto-discover task modules via rglob (no more hardcoded imports)
- Coroutine validation after __wrapped__ unwrap
- Canary assertion for CurrentMessage._MESSAGE private API
- E2E_AUTH preset to avoid copy-pasting 5-scope auth decorator
- Autouse _link_user_to_org fixture (reduces test params)
- HTTP guardrail blocking unmocked external calls
- TaskDrainError chains to root cause exception
- Safe Stripe mock defaults (customer, payment method)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename test files to be descriptive:
- test_happy_path.py → test_fixed_price.py (both one_time and subscription)

Implement post_purchase/test_seat_claim.py:
- Buy 3 seats on a seat-based product
- Assign a seat to a teammate via API
- Retrieve invitation token from DB
- Claim seat with token → status=claimed, benefits enqueued

Implement lifecycle/test_renewal.py:
- Create active subscription with period ending now
- Call subscription_service.cycle() directly
- Drain order.create_subscription_order task
- Verify new order with billing_reason=subscription_cycle

Add order.invoice to DEFAULT_IGNORED_ACTORS (requires S3/MinIO).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Import BUYER_EMAIL/NAME/BILLING_ADDRESS from purchase/conftest
  instead of redefining in test_seat_claim.py
- Move E2E_SEAT_AUTH to post_purchase/conftest.py
- Remove E2E_LIFECYCLE_AUTH, use E2E_AUTH directly (superset)
- Import monthly_product fixture from subscription/conftest
  into lifecycle/conftest instead of redefining
- Move UserOrganization import to top-level in test_seat_claim.py
- Remove unnecessary sorted() wrapping rglob in task discovery

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of calling subscription_service.cycle() directly, the test now
enqueues the subscription.cycle task through the drain pipeline — the
same way APScheduler triggers it in production.

Add trigger_subscription_cycle() helper in lifecycle/conftest.py that
enqueues via JobQueueManager and returns the drain result, consistent
with how simulate_stripe_webhook works for purchase tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New test: post_purchase/test_benefit_grants.py (Option C pattern)
- test_custom_benefit_granted: no-op strategy, grant record created
- test_license_key_generated: key ID + display key stored on grant
- test_meter_credit_applied: 100 units credited to meter
- test_feature_flag_granted: presence-based grant exists

Shared _purchase_product() helper eliminates purchase flow duplication
across benefit type tests. Each test method has clear Given/When/Then.

Updated all existing tests with Given/When/Then comments, removed
redundant section separators and narrating comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E2E tests should only assert on client-visible outcomes: HTTP status,
database state (via API), and emails sent. Internal details like which
Dramatiq tasks ran are implementation details that couple tests to
the task graph structure.

Removed all `assert "actor.name" in executed` patterns. The drain()
is still called (it raises TaskDrainError on failure) but its result
is no longer inspected for specific task names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a fixes

1. Extract complete_purchase() helper + CompletedPurchase dataclass into
   purchase/conftest.py. Refactored all 8 tests that duplicated the
   checkout-confirm-webhook sequence to use it instead.

2. Add test_payment_failure.py — charge.failed webhook results in no
   order created and checkout returns to open status.

3. Add test_with_tax.py — 10% exclusive tax on $50 product verifies
   tax_amount appears on the order (was hardcoded to 0 in all tests).

4. Fix task_drain.py infrastructure:
   - rglob discovery now excludes tests/, __pycache__, hidden dirs
   - Import errors logged as warnings instead of silently swallowed
   - __wrapped__ unwrapping loops with depth guard (MAX_UNWRAP_DEPTH=10)
   - RuntimeError catch narrowed to "not initialized" message only
   - DrainFn is now a Protocol with full parameter signatures

5. DrainFn Protocol preserves ignored_actors and raise_on_failure
   parameter names for IDE autocomplete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
polar Ready Ready Preview, Comment Apr 3, 2026 7:10am
polar-sandbox Ready Ready Preview, Comment Apr 3, 2026 7:10am

Request Review

Comment on lines +38 to +40
"order.balance",
# Uploads invoice PDF to S3/MinIO
"order.invoice",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need to test those 2. I would love to do it but looks a little bit more complicated

The renewal test now creates the subscription via complete_purchase()
instead of create_subscription() fixture, ensuring the initial state
matches what a real purchase produces. The test verifies both the
original subscription_create order and the renewal subscription_cycle
order exist.

Also adds stripe_service mock to order.service (for renewal payment
trigger) and default create_payment_intent return value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@psincraian psincraian merged commit 0fad0c5 into main Apr 7, 2026
21 checks passed
@psincraian psincraian deleted the worktree-pure-soaring-eclipse branch April 7, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant