@@ -5,6 +5,7 @@ package orgs_test |
| 5 | import ( | 5 | import ( |
| 6 | "context" | 6 | "context" |
| 7 | "encoding/json" | 7 | "encoding/json" |
| | 8 | + "errors" |
| 8 | "net/http" | 9 | "net/http" |
| 9 | "strconv" | 10 | "strconv" |
| 10 | "strings" | 11 | "strings" |
@@ -263,6 +264,39 @@ func TestBillingWebhookSubscriptionUpdatedForUnknownSubReturnsError(t *testing.T |
| 263 | } | 264 | } |
| 264 | } | 265 | } |
| 265 | | 266 | |
| | 267 | +// TestBillingWebhookRejectsBadSignature locks Agent A's untested |
| | 268 | +// claim: a tampered/bad signature returns 400 and writes no row. |
| | 269 | +// The real stripe-go signature check is exercised in production; |
| | 270 | +// this test wires a fake VerifyWebhook that errors and asserts the |
| | 271 | +// handler short-circuits cleanly. |
| | 272 | +func TestBillingWebhookRejectsBadSignature(t *testing.T) { |
| | 273 | + t.Parallel() |
| | 274 | + ctx := context.Background() |
| | 275 | + pool := dbtest.NewTestDB(t) |
| | 276 | + ownerID := insertOrgAvatarUser(t, pool, "owner") |
| | 277 | + _ = insertOrgAvatarOrg(t, pool, ownerID, "acme") |
| | 278 | + |
| | 279 | + fake := &fakeStripeRemote{ |
| | 280 | + verifyWebhookFn: func(_ []byte, _ string) (stripeapi.Event, error) { |
| | 281 | + return stripeapi.Event{}, errors.New("bad signature") |
| | 282 | + }, |
| | 283 | + } |
| | 284 | + mux := newOrgBillingMux(t, pool, ownerID, fake) |
| | 285 | + resp := postBillingWebhook(t, mux, "evt_will_be_rejected") |
| | 286 | + if resp.Code != http.StatusBadRequest { |
| | 287 | + t.Fatalf("bad-sig status=%d body=%s want 400", resp.Code, resp.Body.String()) |
| | 288 | + } |
| | 289 | + // No receipt row should exist — signature failure short-circuits |
| | 290 | + // before RecordWebhookEvent runs. |
| | 291 | + var count int |
| | 292 | + if err := pool.QueryRow(ctx, `SELECT count(*) FROM billing_webhook_events`).Scan(&count); err != nil { |
| | 293 | + t.Fatalf("count receipts: %v", err) |
| | 294 | + } |
| | 295 | + if count != 0 { |
| | 296 | + t.Fatalf("bad-sig should not insert receipt row, got count=%d", count) |
| | 297 | + } |
| | 298 | +} |
| | 299 | + |
| 266 | // TestBillingWebhookDropsStaleEvent locks PRO08 D4: a Stripe event | 300 | // TestBillingWebhookDropsStaleEvent locks PRO08 D4: a Stripe event |
| 267 | // with `created` older than the persisted last_event_at must NOT | 301 | // with `created` older than the persisted last_event_at must NOT |
| 268 | // regress state. Pre-PRO08 a reverse-ordered retry could re-activate | 302 | // regress state. Pre-PRO08 a reverse-ordered retry could re-activate |