@@ -185,6 +185,84 @@ func TestBillingWebhookGuardRefusesTeamPriceOnUserSubject(t *testing.T) { |
| 185 | 185 | } |
| 186 | 186 | } |
| 187 | 187 | |
| 188 | +// TestBillingWebhookSubscriptionDeletedForUnknownSubIsNoOp locks PRO08 |
| 189 | +// D5: when Stripe sends customer.subscription.deleted for a subscription |
| 190 | +// shithub has never seen (no metadata, no customer-id match, no |
| 191 | +// subscription-id match), the handler logs and returns 200 so Stripe |
| 192 | +// stops retrying. Other event types still 5xx to surface misconfig. |
| 193 | +func TestBillingWebhookSubscriptionDeletedForUnknownSubIsNoOp(t *testing.T) { |
| 194 | + t.Parallel() |
| 195 | + ctx := context.Background() |
| 196 | + pool := dbtest.NewTestDB(t) |
| 197 | + ownerID := insertOrgAvatarUser(t, pool, "owner") |
| 198 | + |
| 199 | + // No metadata, no customer-id we've seen, no subscription-id we've |
| 200 | + // seen. resolvePrincipalFromSubscription returns ErrPrincipalNotFound. |
| 201 | + raw, err := json.Marshal(map[string]any{ |
| 202 | + "id": "sub_unknown", |
| 203 | + "customer": "cus_unknown", |
| 204 | + "status": "canceled", |
| 205 | + "items": map[string]any{"data": []map[string]any{}}, |
| 206 | + }) |
| 207 | + if err != nil { |
| 208 | + t.Fatalf("marshal: %v", err) |
| 209 | + } |
| 210 | + fake := &fakeStripeRemote{ |
| 211 | + verifyWebhookFn: func(_ []byte, _ string) (stripeapi.Event, error) { |
| 212 | + return stripeapi.Event{ |
| 213 | + ID: "evt_unknown_delete", |
| 214 | + Type: stripeapi.EventType("customer.subscription.deleted"), |
| 215 | + Data: &stripeapi.EventData{Raw: raw}, |
| 216 | + }, nil |
| 217 | + }, |
| 218 | + } |
| 219 | + mux := newOrgBillingMux(t, pool, ownerID, fake) |
| 220 | + resp := postBillingWebhook(t, mux, "evt_unknown_delete") |
| 221 | + if resp.Code != http.StatusOK { |
| 222 | + t.Fatalf("unknown-sub delete status=%d body=%s (expected 200 no-op)", resp.Code, resp.Body.String()) |
| 223 | + } |
| 224 | + receipt, err := billingdb.New().GetWebhookEventReceipt(ctx, pool, "evt_unknown_delete") |
| 225 | + if err != nil { |
| 226 | + t.Fatalf("get receipt: %v", err) |
| 227 | + } |
| 228 | + if !receipt.ProcessedAt.Valid { |
| 229 | + t.Fatalf("receipt should be marked processed (no retries needed), got %+v", receipt) |
| 230 | + } |
| 231 | +} |
| 232 | + |
| 233 | +// TestBillingWebhookSubscriptionUpdatedForUnknownSubReturnsError is |
| 234 | +// the contrast: subscription.updated for an unknown sub still 5xx's |
| 235 | +// (operator should hear about it). |
| 236 | +func TestBillingWebhookSubscriptionUpdatedForUnknownSubReturnsError(t *testing.T) { |
| 237 | + t.Parallel() |
| 238 | + pool := dbtest.NewTestDB(t) |
| 239 | + ownerID := insertOrgAvatarUser(t, pool, "owner") |
| 240 | + |
| 241 | + raw, err := json.Marshal(map[string]any{ |
| 242 | + "id": "sub_unknown_update", |
| 243 | + "customer": "cus_unknown_update", |
| 244 | + "status": "active", |
| 245 | + "items": map[string]any{"data": []map[string]any{}}, |
| 246 | + }) |
| 247 | + if err != nil { |
| 248 | + t.Fatalf("marshal: %v", err) |
| 249 | + } |
| 250 | + fake := &fakeStripeRemote{ |
| 251 | + verifyWebhookFn: func(_ []byte, _ string) (stripeapi.Event, error) { |
| 252 | + return stripeapi.Event{ |
| 253 | + ID: "evt_unknown_update", |
| 254 | + Type: stripeapi.EventType("customer.subscription.updated"), |
| 255 | + Data: &stripeapi.EventData{Raw: raw}, |
| 256 | + }, nil |
| 257 | + }, |
| 258 | + } |
| 259 | + mux := newOrgBillingMux(t, pool, ownerID, fake) |
| 260 | + resp := postBillingWebhook(t, mux, "evt_unknown_update") |
| 261 | + if resp.Code != http.StatusInternalServerError { |
| 262 | + t.Fatalf("unknown-sub update status=%d body=%s (expected 5xx for operator visibility)", resp.Code, resp.Body.String()) |
| 263 | + } |
| 264 | +} |
| 265 | + |
| 188 | 266 | // TestBillingWebhookDropsStaleEvent locks PRO08 D4: a Stripe event |
| 189 | 267 | // with `created` older than the persisted last_event_at must NOT |
| 190 | 268 | // regress state. Pre-PRO08 a reverse-ordered retry could re-activate |