tenseleyflow/shithub / 3b56264

Browse files

orgs tests: stale (reverse-ordered) Stripe event is dropped without regressing state

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3b56264d33b728f3aa426f837bee18851fae3ab1
Parents
1073b56
Tree
439ae16

1 changed file

StatusFile+-
M internal/web/handlers/orgs/billing_webhook_guard_test.go 66 0
internal/web/handlers/orgs/billing_webhook_guard_test.gomodified
@@ -185,6 +185,72 @@ func TestBillingWebhookGuardRefusesTeamPriceOnUserSubject(t *testing.T) {
185185
 	}
186186
 }
187187
 
188
+// TestBillingWebhookDropsStaleEvent locks PRO08 D4: a Stripe event
189
+// with `created` older than the persisted last_event_at must NOT
190
+// regress state. Pre-PRO08 a reverse-ordered retry could re-activate
191
+// a canceled subscription. The handler returns 200 (Stripe stops
192
+// retrying THIS delivery) and leaves state alone.
193
+func TestBillingWebhookDropsStaleEvent(t *testing.T) {
194
+	t.Parallel()
195
+	ctx := context.Background()
196
+	pool := dbtest.NewTestDB(t)
197
+	ownerID := insertOrgAvatarUser(t, pool, "owner")
198
+	orgID := insertOrgAvatarOrg(t, pool, ownerID, "acme")
199
+
200
+	// Establish a fresh canceled state via direct apply + touch.
201
+	if _, err := orgbilling.MarkCanceledForPrincipal(ctx, orgbilling.Deps{Pool: pool}, orgbilling.PrincipalForOrg(orgID), "evt_canceled"); err != nil {
202
+		t.Fatalf("MarkCanceled: %v", err)
203
+	}
204
+	freshTime := time.Now().UTC()
205
+	if err := orgbilling.TouchBillingLastEventAtForPrincipal(ctx, orgbilling.Deps{Pool: pool}, orgbilling.PrincipalForOrg(orgID), freshTime); err != nil {
206
+		t.Fatalf("touch fresh: %v", err)
207
+	}
208
+
209
+	// A stale (older) subscription.updated[active] arrives. event.Created
210
+	// is 1 hour BEFORE the persisted last_event_at.
211
+	staleCreated := freshTime.Add(-1 * time.Hour).Unix()
212
+	raw, err := json.Marshal(map[string]any{
213
+		"id":       "sub_stale_active",
214
+		"customer": "cus_stale",
215
+		"status":   "active",
216
+		"metadata": map[string]string{stripebilling.MetadataOrgID: strconv.FormatInt(orgID, 10)},
217
+		"items": map[string]any{"data": []map[string]any{{
218
+			"id":                   "si_stale",
219
+			"current_period_start": time.Now().UTC().Add(-time.Hour).Unix(),
220
+			"current_period_end":   time.Now().UTC().Add(30 * 24 * time.Hour).Unix(),
221
+			"price":                map[string]string{"id": testTeamPriceID},
222
+		}}},
223
+	})
224
+	if err != nil {
225
+		t.Fatalf("marshal: %v", err)
226
+	}
227
+	fake := &fakeStripeRemote{
228
+		verifyWebhookFn: func(_ []byte, _ string) (stripeapi.Event, error) {
229
+			return stripeapi.Event{
230
+				ID:      "evt_stale_active",
231
+				Type:    stripeapi.EventType("customer.subscription.updated"),
232
+				Created: staleCreated,
233
+				Data:    &stripeapi.EventData{Raw: raw},
234
+			}, nil
235
+		},
236
+	}
237
+	mux := newOrgBillingMuxWithPrices(t, pool, ownerID, fake, testTeamPriceID, testProPriceID)
238
+	resp := postBillingWebhook(t, mux, "evt_stale_active")
239
+	if resp.Code != http.StatusOK {
240
+		t.Fatalf("stale event status=%d body=%s", resp.Code, resp.Body.String())
241
+	}
242
+	state, err := orgbilling.GetOrgBillingState(ctx, orgbilling.Deps{Pool: pool}, orgID)
243
+	if err != nil {
244
+		t.Fatalf("get: %v", err)
245
+	}
246
+	if state.Plan != orgbilling.PlanFree {
247
+		t.Fatalf("stale event corrupted state: plan=%s want free", state.Plan)
248
+	}
249
+	if state.SubscriptionStatus != orgbilling.SubscriptionStatusCanceled {
250
+		t.Fatalf("stale event corrupted status: got %s want canceled", state.SubscriptionStatus)
251
+	}
252
+}
253
+
188254
 // TestBillingWebhookGuardRefusesSecondSubscriptionForSameCustomer locks
189255
 // PRO08 D3: when the principal already has a Stripe subscription on
190256
 // file, a webhook event referencing a DIFFERENT subscription must be