@@ -5,6 +5,7 @@ package orgs_test |
| 5 | 5 | import ( |
| 6 | 6 | "context" |
| 7 | 7 | "encoding/json" |
| 8 | + "errors" |
| 8 | 9 | "net/http" |
| 9 | 10 | "strconv" |
| 10 | 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 | 300 | // TestBillingWebhookDropsStaleEvent locks PRO08 D4: a Stripe event |
| 267 | 301 | // with `created` older than the persisted last_event_at must NOT |
| 268 | 302 | // regress state. Pre-PRO08 a reverse-ordered retry could re-activate |