Skip to main content

max / makenotwork

test: migrate stripe webhook tests to rc.5 JSON fixtures Replace typed stripe::Event / EventObject / CheckoutSession / Charge / Subscription / Invoice construction with raw serde_json::json! payloads. The webhook handler only cares about the JSON shape, and rc.5 either removed these types or dropped their Default impls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-17 01:50 UTC
Commit: d13df6be8b030d474dfefec72f325c01c81e8923
Parent: 7effb40
5 files changed, +250 insertions, -340 deletions
@@ -417,22 +417,19 @@ async fn bundle_paid_checkout_grants_child_access() {
417 417 meta.insert("buyer_id".to_string(), buyer_id.to_string());
418 418 meta.insert("seller_id".to_string(), user_id.to_string());
419 419 meta.insert("item_id".to_string(), bundle_id.clone());
420 - let session = stripe::CheckoutSession {
421 - id: session_id.parse().unwrap(),
422 - metadata: Some(meta),
423 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_bundle_001")).unwrap()),
424 - ..Default::default()
425 - };
426 - let event = stripe::Event {
427 - id: "evt_bundle_001".parse().unwrap(),
428 - type_: stripe::EventType::CheckoutSessionCompleted,
429 - data: stripe::NotificationEventData {
430 - object: stripe::EventObject::CheckoutSession(session),
431 - ..Default::default()
432 - },
433 - ..Default::default()
434 - };
435 - let payload = serde_json::to_string(&event).unwrap();
420 + let session = serde_json::json!({
421 + "id": session_id,
422 + "object": "checkout_session",
423 + "mode": "payment",
424 + "metadata": meta,
425 + "payment_intent": "pi_bundle_001",
426 + });
427 + let payload = serde_json::json!({
428 + "id": "evt_bundle_001",
429 + "type": "checkout.session.completed",
430 + "data": {"object": session},
431 + })
432 + .to_string();
436 433 let signature = crate::harness::stripe::sign_webhook_payload(
437 434 &payload,
438 435 crate::harness::stripe::TEST_WEBHOOK_SECRET,
@@ -7,7 +7,6 @@ use crate::harness::TestHarness;
7 7 use makenotwork::db;
8 8 use serde_json::Value;
9 9 use std::collections::HashMap;
10 - use stripe::{Event, EventObject, EventType, NotificationEventData};
11 10
12 11 // ---------------------------------------------------------------------------
13 12 // Helpers
@@ -49,9 +48,18 @@ async fn setup_paid_item(h: &mut TestHarness, price_cents: i32) -> (db::UserId,
49 48 (seller_id, project_id, item_id)
50 49 }
51 50
52 - /// Post a signed webhook event to the harness.
53 - async fn post_webhook(h: &mut TestHarness, event: &Event) -> crate::harness::client::TestResponse {
54 - let payload = serde_json::to_string(event).expect("Event serialization");
51 + /// Post a JSON webhook event to the harness.
52 + async fn post_webhook_json(
53 + h: &mut TestHarness,
54 + event_type: &str,
55 + object: serde_json::Value,
56 + ) -> crate::harness::client::TestResponse {
57 + let payload = serde_json::json!({
58 + "id": "evt_mock_001",
59 + "type": event_type,
60 + "data": {"object": object},
61 + })
62 + .to_string();
55 63 let signature = crate::harness::stripe::sign_webhook_payload(
56 64 &payload,
57 65 crate::harness::stripe::TEST_WEBHOOK_SECRET,
@@ -67,18 +75,6 @@ async fn post_webhook(h: &mut TestHarness, event: &Event) -> crate::harness::cli
67 75 ).await
68 76 }
69 77
70 - fn make_event(event_type: EventType, object: EventObject) -> Event {
71 - Event {
72 - id: "evt_mock_001".parse().unwrap(),
73 - type_: event_type,
74 - data: NotificationEventData {
75 - object,
76 - ..Default::default()
77 - },
78 - ..Default::default()
79 - }
80 - }
81 -
82 78 // ---------------------------------------------------------------------------
83 79 // Checkout → Webhook → Access flow
84 80 // ---------------------------------------------------------------------------
@@ -126,15 +122,15 @@ async fn checkout_creates_session_and_webhook_completes_purchase() {
126 122 meta.insert("buyer_id".to_string(), buyer_id.to_string());
127 123 meta.insert("seller_id".to_string(), seller_id.to_string());
128 124 meta.insert("item_id".to_string(), item_id.clone());
129 - let session = stripe::CheckoutSession {
130 - id: actual_session_id.parse().unwrap(),
131 - metadata: Some(meta),
132 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_mock_001")).unwrap()),
133 - ..Default::default()
134 - };
135 -
136 - let event = make_event(EventType::CheckoutSessionCompleted, EventObject::CheckoutSession(session));
137 - let resp = post_webhook(&mut h, &event).await;
125 + let session = serde_json::json!({
126 + "id": actual_session_id,
127 + "object": "checkout_session",
128 + "mode": "payment",
129 + "metadata": meta,
130 + "payment_intent": "pi_mock_001",
131 + });
132 +
133 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
138 134 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
139 135
140 136 // Verify transaction completed
@@ -196,14 +192,14 @@ async fn purchase_webhook_sends_emails() {
196 192 meta.insert("buyer_id".to_string(), buyer_id.to_string());
197 193 meta.insert("seller_id".to_string(), seller_id.to_string());
198 194 meta.insert("item_id".to_string(), item_id.clone());
199 - let session = stripe::CheckoutSession {
200 - id: session_id.parse().unwrap(),
201 - metadata: Some(meta),
202 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_email_test")).unwrap()),
203 - ..Default::default()
204 - };
205 - let event = make_event(EventType::CheckoutSessionCompleted, EventObject::CheckoutSession(session));
206 - let resp = post_webhook(&mut h, &event).await;
195 + let session = serde_json::json!({
196 + "id": session_id,
197 + "object": "checkout_session",
198 + "mode": "payment",
199 + "metadata": meta,
200 + "payment_intent": "pi_email_test",
201 + });
202 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
207 203 assert_eq!(resp.status.as_u16(), 200);
208 204
209 205 // Wait briefly for fire-and-forget email tasks to complete
@@ -369,14 +365,14 @@ async fn purchase_grants_access() {
369 365 meta.insert("buyer_id".to_string(), buyer_id.to_string());
370 366 meta.insert("seller_id".to_string(), seller_id.to_string());
371 367 meta.insert("item_id".to_string(), item_id.clone());
372 - let session = stripe::CheckoutSession {
373 - id: session_id.parse().unwrap(),
374 - metadata: Some(meta),
375 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_access_001")).unwrap()),
376 - ..Default::default()
377 - };
378 - let event = make_event(EventType::CheckoutSessionCompleted, EventObject::CheckoutSession(session));
379 - let resp = post_webhook(&mut h, &event).await;
368 + let session = serde_json::json!({
369 + "id": session_id,
370 + "object": "checkout_session",
371 + "mode": "payment",
372 + "metadata": meta,
373 + "payment_intent": "pi_access_001",
374 + });
375 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
380 376 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
381 377
382 378 // Verify buyer has access via has_purchased_item query
@@ -428,13 +424,14 @@ async fn refund_revokes_access() {
428 424 .unwrap();
429 425
430 426 // Fire ChargeRefunded webhook
431 - let charge = stripe::Charge {
432 - id: "ch_refund_mock".parse().unwrap(),
433 - payment_intent: Some(serde_json::from_value(serde_json::json!(pi_id)).unwrap()),
434 - ..Default::default()
435 - };
436 - let event = make_event(EventType::ChargeRefunded, EventObject::Charge(charge));
437 - let resp = post_webhook(&mut h, &event).await;
427 + let charge = serde_json::json!({
428 + "id": "ch_refund_mock",
429 + "object": "charge",
430 + "amount": 999,
431 + "amount_refunded": 999,
432 + "payment_intent": pi_id,
433 + });
434 + let resp = post_webhook_json(&mut h, "charge.refunded", charge).await;
438 435 assert_eq!(resp.status.as_u16(), 200, "Refund webhook failed: {}", resp.text);
439 436
440 437 // Verify transaction status is 'refunded'
@@ -508,14 +505,14 @@ async fn tip_checkout_and_webhook() {
508 505 meta.insert("checkout_type".to_string(), "tip".to_string());
509 506 meta.insert("tipper_id".to_string(), tipper_id.to_string());
510 507 meta.insert("recipient_id".to_string(), recipient_id.to_string());
511 - let session = stripe::CheckoutSession {
512 - id: tip_session_id.parse().unwrap(),
513 - metadata: Some(meta),
514 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_tip_001")).unwrap()),
515 - ..Default::default()
516 - };
517 - let event = make_event(EventType::CheckoutSessionCompleted, EventObject::CheckoutSession(session));
518 - let resp = post_webhook(&mut h, &event).await;
508 + let session = serde_json::json!({
509 + "id": tip_session_id,
510 + "object": "checkout_session",
511 + "mode": "payment",
512 + "metadata": meta,
513 + "payment_intent": "pi_tip_001",
514 + });
515 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
519 516 assert_eq!(resp.status.as_u16(), 200, "Tip webhook failed: {}", resp.text);
520 517
521 518 // Verify tip status is 'completed'
@@ -575,14 +572,14 @@ async fn revenue_splits_recorded_on_purchase() {
575 572 meta.insert("buyer_id".to_string(), buyer_id.to_string());
576 573 meta.insert("seller_id".to_string(), seller_id.to_string());
577 574 meta.insert("item_id".to_string(), item_id.clone());
578 - let session = stripe::CheckoutSession {
579 - id: session_id.parse().unwrap(),
580 - metadata: Some(meta),
581 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_split_001")).unwrap()),
582 - ..Default::default()
583 - };
584 - let event = make_event(EventType::CheckoutSessionCompleted, EventObject::CheckoutSession(session));
585 - let resp = post_webhook(&mut h, &event).await;
575 + let session = serde_json::json!({
576 + "id": session_id,
577 + "object": "checkout_session",
578 + "mode": "payment",
579 + "metadata": meta,
580 + "payment_intent": "pi_split_001",
581 + });
582 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
586 583 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
587 584
588 585 // Wait briefly for split recording (runs after transaction commit)
@@ -730,14 +727,14 @@ async fn contact_sharing_on_purchase() {
730 727 meta.insert("buyer_id".to_string(), buyer_id.to_string());
731 728 meta.insert("seller_id".to_string(), seller_id.to_string());
732 729 meta.insert("item_id".to_string(), item_id.clone());
733 - let session = stripe::CheckoutSession {
734 - id: session_id.parse().unwrap(),
735 - metadata: Some(meta),
736 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_contact_001")).unwrap()),
737 - ..Default::default()
738 - };
739 - let event = make_event(EventType::CheckoutSessionCompleted, EventObject::CheckoutSession(session));
740 - let resp = post_webhook(&mut h, &event).await;
730 + let session = serde_json::json!({
731 + "id": session_id,
732 + "object": "checkout_session",
733 + "mode": "payment",
734 + "metadata": meta,
735 + "payment_intent": "pi_contact_001",
736 + });
737 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
741 738 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
742 739
743 740 // Verify transaction completed and still has share_contact = true
@@ -8,7 +8,6 @@ use crate::harness::TestHarness;
8 8 use makenotwork::db;
9 9 use serde_json::Value;
10 10 use std::collections::HashMap;
11 - use stripe::{EventObject, EventType, NotificationEventData};
12 11
13 12 #[tokio::test]
14 13 async fn free_item_library_flow() {
@@ -148,12 +147,18 @@ async fn setup_creator_with_paid_item(
148 147 (seller_id, project_id, item_id)
149 148 }
150 149
151 - /// Post a signed webhook event to the harness.
152 - async fn post_webhook(
150 + /// Post a JSON webhook event to the harness.
151 + async fn post_webhook_json(
153 152 h: &mut TestHarness,
154 - event: &stripe::Event,
153 + event_type: &str,
154 + object: serde_json::Value,
155 155 ) -> crate::harness::client::TestResponse {
156 - let payload = serde_json::to_string(event).expect("Event serialization");
156 + let payload = serde_json::json!({
157 + "id": "evt_purchase_test",
158 + "type": event_type,
159 + "data": {"object": object},
160 + })
161 + .to_string();
157 162 let signature = crate::harness::stripe::sign_webhook_payload(
158 163 &payload,
159 164 crate::harness::stripe::TEST_WEBHOOK_SECRET,
@@ -171,21 +176,6 @@ async fn post_webhook(
171 176 .await
172 177 }
173 178
174 - fn make_checkout_event(
175 - event_type: EventType,
176 - object: EventObject,
177 - ) -> stripe::Event {
178 - stripe::Event {
179 - id: "evt_purchase_test".parse().unwrap(),
180 - type_: event_type,
181 - data: NotificationEventData {
182 - object,
183 - ..Default::default()
184 - },
185 - ..Default::default()
186 - }
187 - }
188 -
189 179 // ---------------------------------------------------------------------------
190 180 // 1. Paid purchase via mock Stripe
191 181 // ---------------------------------------------------------------------------
@@ -236,19 +226,14 @@ async fn paid_purchase_via_mock_stripe() {
236 226 meta.insert("buyer_id".to_string(), buyer_id.to_string());
237 227 meta.insert("seller_id".to_string(), seller_id.to_string());
238 228 meta.insert("item_id".to_string(), item_id.clone());
239 - let session = stripe::CheckoutSession {
240 - id: session_id.parse().unwrap(),
241 - metadata: Some(meta),
242 - payment_intent: Some(
243 - serde_json::from_value(serde_json::json!("pi_paid_001")).unwrap(),
244 - ),
245 - ..Default::default()
246 - };
247 - let event = make_checkout_event(
248 - EventType::CheckoutSessionCompleted,
249 - EventObject::CheckoutSession(session),
250 - );
251 - let resp = post_webhook(&mut h, &event).await;
229 + let session = serde_json::json!({
230 + "id": session_id,
231 + "object": "checkout_session",
232 + "mode": "payment",
233 + "metadata": meta,
234 + "payment_intent": "pi_paid_001",
235 + });
236 + let resp = post_webhook_json(&mut h, "checkout.session.completed", session).await;
252 237 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
253 238
254 239 // Verify transaction completed
@@ -5,7 +5,6 @@ use crate::harness::TestHarness;
5 5 use makenotwork::db;
6 6 use serde_json::Value;
7 7 use std::collections::HashMap;
8 - use stripe::{Event, EventObject, EventType, NotificationEventData};
9 8
10 9 // ---------------------------------------------------------------------------
11 10 // Helpers (mirrors mock_payment_flows patterns)
@@ -45,20 +44,17 @@ async fn setup_paid_item(h: &mut TestHarness, price_cents: i32) -> (db::UserId,
45 44 (seller_id, project_id, item_id)
46 45 }
47 46
48 - fn make_event(event_type: EventType, object: EventObject) -> Event {
49 - Event {
50 - id: "evt_split_test".parse().unwrap(),
51 - type_: event_type,
52 - data: NotificationEventData {
53 - object,
54 - ..Default::default()
55 - },
56 - ..Default::default()
57 - }
58 - }
59 -
60 - async fn post_webhook(h: &mut TestHarness, event: &Event) -> crate::harness::client::TestResponse {
61 - let payload = serde_json::to_string(event).expect("Event serialization");
47 + async fn post_webhook_json(
48 + h: &mut TestHarness,
49 + event_type: &str,
50 + object: serde_json::Value,
51 + ) -> crate::harness::client::TestResponse {
52 + let payload = serde_json::json!({
53 + "id": "evt_split_test",
54 + "type": event_type,
55 + "data": {"object": object},
56 + })
57 + .to_string();
62 58 let signature = crate::harness::stripe::sign_webhook_payload(
63 59 &payload,
64 60 crate::harness::stripe::TEST_WEBHOOK_SECRET,
@@ -102,20 +98,14 @@ async fn complete_purchase(
102 98 meta.insert("buyer_id".to_string(), buyer_id.to_string());
103 99 meta.insert("seller_id".to_string(), seller_id.to_string());
104 100 meta.insert("item_id".to_string(), item_id.to_string());
105 - let session = stripe::CheckoutSession {
106 - id: session_id.parse().unwrap(),
107 - metadata: Some(meta),
108 - payment_intent: Some(
109 - serde_json::from_value(serde_json::json!(format!("pi_split_{}", buyer_username)))
110 - .unwrap(),
111 - ),
112 - ..Default::default()
113 - };
114 - let event = make_event(
115 - EventType::CheckoutSessionCompleted,
116 - EventObject::CheckoutSession(session),
117 - );
118 - let resp = post_webhook(h, &event).await;
101 + let session = serde_json::json!({
102 + "id": session_id,
103 + "object": "checkout_session",
104 + "mode": "payment",
105 + "metadata": meta,
106 + "payment_intent": format!("pi_split_{}", buyer_username),
107 + });
108 + let resp = post_webhook_json(h, "checkout.session.completed", session).await;
119 109 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
120 110
121 111 buyer_id
@@ -6,11 +6,28 @@ use crate::harness::TestHarness;
6 6 use makenotwork::db::UserId;
7 7 use serde_json::Value;
8 8 use std::collections::HashMap;
9 - use stripe::{Event, EventObject, EventType, NotificationEventData};
10 9
11 - /// Build an Event in-memory, serialize to JSON, sign, and POST to /stripe/webhook.
12 - async fn post_event(h: &mut TestHarness, event: &Event) -> crate::harness::client::TestResponse {
13 - let payload = serde_json::to_string(event).expect("Event serialization");
10 + /// Build a JSON event with the given type and object, sign it, and POST to /stripe/webhook.
11 + async fn post_event_json(
12 + h: &mut TestHarness,
13 + event_type: &str,
14 + object: serde_json::Value,
15 + ) -> crate::harness::client::TestResponse {
16 + post_event_json_with_id(h, "evt_test_000", event_type, object).await
17 + }
18 +
19 + async fn post_event_json_with_id(
20 + h: &mut TestHarness,
21 + event_id: &str,
22 + event_type: &str,
23 + object: serde_json::Value,
24 + ) -> crate::harness::client::TestResponse {
25 + let payload = serde_json::json!({
26 + "id": event_id,
27 + "type": event_type,
28 + "data": {"object": object},
29 + })
30 + .to_string();
14 31 let signature = sign_webhook_payload(&payload, TEST_WEBHOOK_SECRET);
15 32
16 33 h.client
@@ -26,19 +43,6 @@ async fn post_event(h: &mut TestHarness, event: &Event) -> crate::harness::clien
26 43 .await
27 44 }
28 45
29 - /// Construct a minimal Event with the given type and object.
30 - fn make_event(event_type: EventType, object: EventObject) -> Event {
31 - Event {
32 - id: "evt_test_000".parse().unwrap(),
33 - type_: event_type,
34 - data: NotificationEventData {
35 - object,
36 - ..Default::default()
37 - },
38 - ..Default::default()
39 - }
40 - }
41 -
42 46 // ---------------------------------------------------------------------------
43 47 // Tests
44 48 // ---------------------------------------------------------------------------
@@ -85,16 +89,15 @@ async fn webhook_account_updated() {
85 89 .unwrap();
86 90
87 91 // Build account object with valid id prefix
88 - let account: stripe::Account = serde_json::from_value(serde_json::json!({
92 + let account = serde_json::json!({
89 93 "id": acct_id,
94 + "object": "account",
90 95 "charges_enabled": true,
91 96 "payouts_enabled": true,
92 97 "details_submitted": true,
93 - }))
94 - .unwrap();
98 + });
95 99
96 - let event = make_event(EventType::AccountUpdated, EventObject::Account(account));
97 - let resp = post_event(&mut h, &event).await;
100 + let resp = post_event_json(&mut h, "account.updated", account).await;
98 101 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
99 102
100 103 // Verify DB was updated
@@ -161,18 +164,15 @@ async fn webhook_purchase_completed() {
161 164 meta.insert("buyer_id".to_string(), buyer_id.to_string());
162 165 meta.insert("seller_id".to_string(), seller_id.to_string());
163 166 meta.insert("item_id".to_string(), item_id.clone());
164 - let session = stripe::CheckoutSession {
165 - id: session_id.parse().unwrap(),
166 - metadata: Some(meta),
167 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_test_purchase_123")).unwrap()),
168 - ..Default::default()
169 - };
170 -
171 - let event = make_event(
172 - EventType::CheckoutSessionCompleted,
173 - EventObject::CheckoutSession(session),
174 - );
175 - let resp = post_event(&mut h, &event).await;
167 + let session = serde_json::json!({
168 + "id": session_id,
169 + "object": "checkout_session",
170 + "mode": "payment",
171 + "metadata": meta,
172 + "payment_intent": "pi_test_purchase_123",
173 + });
174 +
175 + let resp = post_event_json(&mut h, "checkout.session.completed", session).await;
176 176 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
177 177
178 178 // Verify transaction was completed
@@ -245,14 +245,15 @@ async fn webhook_charge_refunded() {
245 245 .unwrap();
246 246
247 247 // Build charge with valid id and payment_intent
248 - let charge = stripe::Charge {
249 - id: "ch_test_refund".parse().unwrap(),
250 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_test_refund_123")).unwrap()),
251 - ..Default::default()
252 - };
253 -
254 - let event = make_event(EventType::ChargeRefunded, EventObject::Charge(charge));
255 - let resp = post_event(&mut h, &event).await;
248 + let charge = serde_json::json!({
249 + "id": "ch_test_refund",
250 + "object": "charge",
251 + "amount": 500,
252 + "amount_refunded": 500,
253 + "payment_intent": "pi_test_refund_123",
254 + });
255 +
256 + let resp = post_event_json(&mut h, "charge.refunded", charge).await;
256 257 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
257 258
258 259 // Verify transaction was refunded
@@ -321,37 +322,25 @@ async fn webhook_subscription_deleted() {
321 322 .await
322 323 .unwrap();
323 324
324 - // Build subscription object — must use from_value because Subscription has
325 - // required non-Option fields (customer, items, etc.) whose Default values
326 - // don't survive a serde round-trip (empty IDs fail prefix validation).
327 - let sub: stripe::Subscription = serde_json::from_value(serde_json::json!({
325 + let sub = serde_json::json!({
328 326 "id": stripe_sub_id,
327 + "object": "subscription",
329 328 "status": "canceled",
330 - "customer": "cus_test_sub_123",
331 - "currency": "usd",
332 329 "cancel_at_period_end": false,
333 330 "items": {
334 331 "object": "list",
335 - "data": [],
336 - "has_more": false,
337 - "url": "/v1/subscription_items"
332 + "data": [{
333 + "id": "si_test_del",
334 + "object": "subscription_item",
335 + "subscription": stripe_sub_id,
336 + "current_period_start": 1700000000,
337 + "current_period_end": 1702592000,
338 + "metadata": {},
339 + }],
338 340 },
339 - "automatic_tax": { "enabled": false },
340 - "billing_cycle_anchor": 1700000000,
341 - "current_period_start": 1700000000,
342 - "current_period_end": 1702592000,
343 - "created": 1700000000,
344 - "start_date": 1700000000,
345 - "livemode": false,
346 - "metadata": {},
347 - }))
348 - .expect("Subscription deserialization");
341 + });
349 342
350 - let event = make_event(
351 - EventType::CustomerSubscriptionDeleted,
352 - EventObject::Subscription(sub),
353 - );
354 - let resp = post_event(&mut h, &event).await;
343 + let resp = post_event_json(&mut h, "customer.subscription.deleted", sub).await;
355 344 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
356 345
357 346 // Verify subscription was canceled
@@ -453,19 +442,16 @@ async fn webhook_subscription_checkout_completed() {
453 442 meta.insert("subscriber_id".to_string(), fix.subscriber_id.to_string());
454 443 meta.insert("project_id".to_string(), fix.project_id.clone());
455 444 meta.insert("tier_id".to_string(), fix.tier_id.to_string());
456 - let session = stripe::CheckoutSession {
457 - id: "cs_test_sub_checkout_001".parse().unwrap(),
458 - metadata: Some(meta),
459 - subscription: Some(serde_json::from_value(serde_json::json!(stripe_sub_id)).unwrap()),
460 - customer: Some(serde_json::from_value(serde_json::json!(stripe_customer_id)).unwrap()),
461 - ..Default::default()
462 - };
445 + let session = serde_json::json!({
446 + "id": "cs_test_sub_checkout_001",
447 + "object": "checkout_session",
448 + "mode": "subscription",
449 + "metadata": meta,
450 + "subscription": stripe_sub_id,
451 + "customer": stripe_customer_id,
452 + });
463 453
464 - let event = make_event(
465 - EventType::CheckoutSessionCompleted,
466 - EventObject::CheckoutSession(session),
467 - );
468 - let resp = post_event(&mut h, &event).await;
454 + let resp = post_event_json(&mut h, "checkout.session.completed", session).await;
469 455 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
470 456
471 457 // Verify subscription row was created
@@ -496,30 +482,22 @@ async fn webhook_subscription_checkout_completed_idempotent() {
496 482 meta.insert("subscriber_id".to_string(), fix.subscriber_id.to_string());
497 483 meta.insert("project_id".to_string(), fix.project_id.clone());
498 484 meta.insert("tier_id".to_string(), fix.tier_id.to_string());
499 - stripe::CheckoutSession {
500 - id: "cs_test_sub_idem".parse().unwrap(),
501 - metadata: Some(meta),
502 - subscription: Some(serde_json::from_value(serde_json::json!(stripe_sub_id)).unwrap()),
503 - customer: Some(serde_json::from_value(serde_json::json!(stripe_customer_id)).unwrap()),
504 - ..Default::default()
505 - }
485 + serde_json::json!({
486 + "id": "cs_test_sub_idem",
487 + "object": "checkout_session",
488 + "mode": "subscription",
489 + "metadata": meta,
490 + "subscription": stripe_sub_id,
491 + "customer": stripe_customer_id,
492 + })
506 493 };
507 494
508 495 // First event
509 - let event1 = make_event(
510 - EventType::CheckoutSessionCompleted,
511 - EventObject::CheckoutSession(build_session()),
512 - );
513 - let resp = post_event(&mut h, &event1).await;
496 + let resp = post_event_json(&mut h, "checkout.session.completed", build_session()).await;
514 497 assert_eq!(resp.status.as_u16(), 200, "First webhook failed: {}", resp.text);
515 498
516 499 // Second event (duplicate) — use a different event ID
517 - let mut event2 = make_event(
518 - EventType::CheckoutSessionCompleted,
519 - EventObject::CheckoutSession(build_session()),
520 - );
521 - event2.id = "evt_test_001".parse().unwrap();
522 - let resp = post_event(&mut h, &event2).await;
500 + let resp = post_event_json_with_id(&mut h, "evt_test_001", "checkout.session.completed", build_session()).await;
523 501 assert_eq!(resp.status.as_u16(), 200, "Duplicate webhook should succeed: {}", resp.text);
524 502
525 503 // Verify still only one subscription row
@@ -541,35 +519,25 @@ async fn webhook_subscription_updated() {
541 519 let stripe_sub_id = "sub_test_updated_001";
542 520 insert_active_subscription(&h, &fix, stripe_sub_id).await;
543 521
544 - // Build subscription with past_due status and new period
545 - let sub: stripe::Subscription = serde_json::from_value(serde_json::json!({
522 + let sub = serde_json::json!({
546 523 "id": stripe_sub_id,
524 + "object": "subscription",
547 525 "status": "past_due",
548 - "customer": "cus_test_fixture",
549 - "currency": "usd",
550 526 "cancel_at_period_end": false,
551 527 "items": {
552 528 "object": "list",
553 - "data": [],
554 - "has_more": false,
555 - "url": "/v1/subscription_items"
529 + "data": [{
530 + "id": "si_test_upd",
531 + "object": "subscription_item",
532 + "subscription": stripe_sub_id,
533 + "current_period_start": 1702592000,
534 + "current_period_end": 1705184000,
535 + "metadata": {},
536 + }],
556 537 },
557 - "automatic_tax": { "enabled": false },
558 - "billing_cycle_anchor": 1700000000,
559 - "current_period_start": 1702592000,
560 - "current_period_end": 1705184000,
561 - "created": 1700000000,
562 - "start_date": 1700000000,
563 - "livemode": false,
564 - "metadata": {},
565 - }))
566 - .expect("Subscription deserialization");
538 + });
567 539
568 - let event = make_event(
569 - EventType::CustomerSubscriptionUpdated,
570 - EventObject::Subscription(sub),
571 - );
572 - let resp = post_event(&mut h, &event).await;
540 + let resp = post_event_json(&mut h, "customer.subscription.updated", sub).await;
573 541 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
574 542
575 543 // Verify status changed
@@ -604,22 +572,17 @@ async fn webhook_invoice_payment_succeeded() {
604 572 let stripe_sub_id = "sub_test_inv_success";
605 573 insert_active_subscription(&h, &fix, stripe_sub_id).await;
606 574
607 - // Build invoice with subscription reference and period
608 - let invoice: stripe::Invoice = serde_json::from_value(serde_json::json!({
575 + let invoice = serde_json::json!({
609 576 "id": "in_test_success_001",
577 + "object": "invoice",
610 578 "subscription": stripe_sub_id,
611 579 "period_start": 1702592000,
612 580 "period_end": 1705184000,
613 581 "billing_reason": "subscription_cycle",
614 582 "livemode": false,
615 - }))
616 - .expect("Invoice deserialization");
583 + });
617 584
618 - let event = make_event(
619 - EventType::InvoicePaymentSucceeded,
620 - EventObject::Invoice(invoice),
621 - );
622 - let resp = post_event(&mut h, &event).await;
585 + let resp = post_event_json(&mut h, "invoice.payment_succeeded", invoice).await;
623 586 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
624 587
625 588 // Verify subscription period was updated
@@ -644,19 +607,16 @@ async fn webhook_invoice_payment_failed() {
644 607 let stripe_sub_id = "sub_test_inv_failed";
645 608 insert_active_subscription(&h, &fix, stripe_sub_id).await;
646 609
647 - // Build invoice with subscription reference
648 - let invoice: stripe::Invoice = serde_json::from_value(serde_json::json!({
610 + let invoice = serde_json::json!({
649 611 "id": "in_test_failed_001",
612 + "object": "invoice",
650 613 "subscription": stripe_sub_id,
614 + "period_start": 1700000000,
615 + "period_end": 1702592000,
651 616 "livemode": false,
652 - }))
653 - .expect("Invoice deserialization");
617 + });
654 618
655 - let event = make_event(
656 - EventType::InvoicePaymentFailed,
657 - EventObject::Invoice(invoice),
658 - );
659 - let resp = post_event(&mut h, &event).await;
619 + let resp = post_event_json(&mut h, "invoice.payment_failed", invoice).await;
660 620 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
661 621
662 622 // Verify status changed to past_due
@@ -684,16 +644,15 @@ async fn webhook_account_updated_partial() {
684 644 .unwrap();
685 645
686 646 // Only details_submitted is true; charges and payouts still false
687 - let account: stripe::Account = serde_json::from_value(serde_json::json!({
647 + let account = serde_json::json!({
688 648 "id": acct_id,
649 + "object": "account",
689 650 "charges_enabled": false,
690 651 "payouts_enabled": false,
691 652 "details_submitted": true,
692 - }))
693 - .unwrap();
653 + });
694 654
695 - let event = make_event(EventType::AccountUpdated, EventObject::Account(account));
696 - let resp = post_event(&mut h, &event).await;
655 + let resp = post_event_json(&mut h, "account.updated", account).await;
697 656 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
698 657
699 658 let (charges, payouts, onboarding): (bool, bool, bool) = sqlx::query_as(
@@ -714,16 +673,15 @@ async fn webhook_account_updated_unknown_account() {
714 673 let mut h = TestHarness::with_stripe().await;
715 674
716 675 // No user has this stripe_account_id
717 - let account: stripe::Account = serde_json::from_value(serde_json::json!({
676 + let account = serde_json::json!({
718 677 "id": "acct_nonexistent",
678 + "object": "account",
719 679 "charges_enabled": true,
720 680 "payouts_enabled": true,
721 681 "details_submitted": true,
722 - }))
723 - .unwrap();
682 + });
724 683
725 - let event = make_event(EventType::AccountUpdated, EventObject::Account(account));
726 - let resp = post_event(&mut h, &event).await;
684 + let resp = post_event_json(&mut h, "account.updated", account).await;
727 685 assert_eq!(
728 686 resp.status.as_u16(),
729 687 200,
@@ -791,29 +749,21 @@ async fn webhook_purchase_completed_idempotent() {
791 749 meta.insert("buyer_id".to_string(), buyer_id.to_string());
792 750 meta.insert("seller_id".to_string(), seller_id.to_string());
793 751 meta.insert("item_id".to_string(), item_id.clone());
794 - stripe::CheckoutSession {
795 - id: session_id.parse().unwrap(),
796 - metadata: Some(meta),
797 - payment_intent: Some(serde_json::from_value(serde_json::json!("pi_test_idem_001")).unwrap()),
798 - ..Default::default()
799 - }
752 + serde_json::json!({
753 + "id": session_id,
754 + "object": "checkout_session",
755 + "mode": "payment",
756 + "metadata": meta,
757 + "payment_intent": "pi_test_idem_001",
758 + })
800 759 };
801 760
802 761 // First event
803 - let event1 = make_event(
804 - EventType::CheckoutSessionCompleted,
805 - EventObject::CheckoutSession(build_session()),
806 - );
807 - let resp = post_event(&mut h, &event1).await;
762 + let resp = post_event_json(&mut h, "checkout.session.completed", build_session()).await;
808 763 assert_eq!(resp.status.as_u16(), 200, "First webhook failed: {}", resp.text);
809 764
810 765 // Second event (duplicate)
811 - let mut event2 = make_event(
812 - EventType::CheckoutSessionCompleted,
813 - EventObject::CheckoutSession(build_session()),
814 - );
815 - event2.id = "evt_test_002".parse().unwrap();
816 - let resp = post_event(&mut h, &event2).await;
766 + let resp = post_event_json_with_id(&mut h, "evt_test_002", "checkout.session.completed", build_session()).await;
817 767 assert_eq!(resp.status.as_u16(), 200, "Duplicate webhook should succeed: {}", resp.text);
818 768
819 769 // Verify still one completed transaction
@@ -934,29 +884,24 @@ async fn webhook_v2_unknown_event_type_returns_200() {
934 884 // fallback path was exercised.
935 885 // ---------------------------------------------------------------------------
936 886
937 - fn make_subscription(stripe_sub_id: &str, status: &str) -> stripe::Subscription {
938 - serde_json::from_value(serde_json::json!({
887 + fn make_subscription(stripe_sub_id: &str, status: &str) -> serde_json::Value {
888 + serde_json::json!({
939 889 "id": stripe_sub_id,
890 + "object": "subscription",
940 891 "status": status,
941 - "customer": "cus_fan_plus_test",
942 - "currency": "usd",
943 892 "cancel_at_period_end": false,
944 893 "items": {
945 894 "object": "list",
946 - "data": [],
947 - "has_more": false,
948 - "url": "/v1/subscription_items"
895 + "data": [{
896 + "id": "si_fan_plus_test",
897 + "object": "subscription_item",
898 + "subscription": stripe_sub_id,
899 + "current_period_start": 1700000000_i64,
900 + "current_period_end": 1702592000_i64,
901 + "metadata": {},
902 + }],
949 903 },
950 - "automatic_tax": { "enabled": false },
951 - "billing_cycle_anchor": 1700000000_i64,
952 - "current_period_start": 1700000000_i64,
953 - "current_period_end": 1702592000_i64,
954 - "created": 1700000000_i64,
955 - "start_date": 1700000000_i64,
956 - "livemode": false,
957 - "metadata": {},
958 - }))
959 - .expect("Subscription deserialization")
904 + })
960 905 }
961 906
962 907 #[tokio::test]
@@ -977,8 +922,7 @@ async fn webhook_subscription_updated_fan_plus_path() {
977 922 .unwrap();
978 923
979 924 let sub = make_subscription(stripe_sub_id, "past_due");
980 - let event = make_event(EventType::CustomerSubscriptionUpdated, EventObject::Subscription(sub));
981 - let resp = post_event(&mut h, &event).await;
925 + let resp = post_event_json(&mut h, "customer.subscription.updated", sub).await;
982 926 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
983 927
984 928 // fan_plus row reflects the new status — pins that the fan_plus branch
@@ -1011,8 +955,7 @@ async fn webhook_subscription_deleted_fan_plus_path() {
1011 955 .unwrap();
1012 956
1013 957 let sub = make_subscription(stripe_sub_id, "canceled");
1014 - let event = make_event(EventType::CustomerSubscriptionDeleted, EventObject::Subscription(sub));
1015 - let resp = post_event(&mut h, &event).await;
958 + let resp = post_event_json(&mut h, "customer.subscription.deleted", sub).await;
1016 959 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
1017 960
1018 961 // Pins that `cancel_fan_plus` ran. We don't pin the exact column the
@@ -1029,8 +972,8 @@ async fn webhook_subscription_deleted_fan_plus_path() {
1029 972 assert!(!active, "Fan+ subscription must not be active after cancellation");
1030 973 }
1031 974
1032 - fn make_invoice(stripe_sub_id: &str, billing_reason: &str) -> stripe::Invoice {
1033 - serde_json::from_value(serde_json::json!({
975 + fn make_invoice(stripe_sub_id: &str, billing_reason: &str) -> serde_json::Value {
976 + serde_json::json!({
1034 977 "id": "in_test_fp",
1035 978 "object": "invoice",
1036 979 "subscription": stripe_sub_id,
@@ -1039,8 +982,7 @@ fn make_invoice(stripe_sub_id: &str, billing_reason: &str) -> stripe::Invoice {
1039 982 "period_end": 1702592000_i64,
1040 983 "currency": "usd",
1041 984 "livemode": false,
1042 - }))
1043 - .expect("Invoice deserialization")
985 + })
1044 986 }
1045 987
1046 988 #[tokio::test]
@@ -1062,8 +1004,7 @@ async fn webhook_invoice_payment_succeeded_updates_fan_plus_period() {
1062 1004
1063 1005 // billing_reason != "subscription_cycle" → not a renewal, just updates period.
1064 1006 let invoice = make_invoice(stripe_sub_id, "subscription_create");
1065 - let event = make_event(EventType::InvoicePaymentSucceeded, EventObject::Invoice(invoice));
1066 - let resp = post_event(&mut h, &event).await;
1007 + let resp = post_event_json(&mut h, "invoice.payment_succeeded", invoice).await;
1067 1008 assert_eq!(resp.status.as_u16(), 200, "Webhook failed: {}", resp.text);
1068 1009
Lines truncated