set('services.revenuecat.webhook', 'shared-secret'); $payload = [ 'event' => [ 'type' => 'INITIAL_PURCHASE', 'app_user_id' => 'tenant-123', 'product_id' => 'pro_month', ], ]; $json = json_encode($payload); $signature = base64_encode(hash_hmac('sha1', $json, 'shared-secret', true)); Bus::fake(); $response = $this->postJson('/api/v1/webhooks/revenuecat', $payload, [ 'X-Event-Id' => 'evt_123', 'X-Signature' => $signature, ]); $response->assertStatus(202) ->assertJson(['status' => 'accepted']); $this->assertDatabaseHas('integration_webhook_events', [ 'provider' => 'revenuecat', 'event_id' => 'evt_123', 'event_type' => 'INITIAL_PURCHASE', 'status' => IntegrationWebhookEvent::STATUS_RECEIVED, ]); Bus::assertDispatched(ProcessRevenueCatWebhook::class); } public function test_webhook_rejects_invalid_signature(): void { config()->set('services.revenuecat.webhook', 'shared-secret'); Bus::fake(); $response = $this->postJson('/api/v1/webhooks/revenuecat', [ 'event' => ['app_user_id' => 'tenant-123'], ], [ 'X-Signature' => 'invalid-signature', ]); $response->assertStatus(400) ->assertJsonPath('error.code', 'signature_invalid') ->assertJsonPath('error.title', 'Invalid Signature'); $this->assertDatabaseCount('integration_webhook_events', 0); Bus::assertNotDispatched(ProcessRevenueCatWebhook::class); } }