$overrides * @return array */ function signedKbzWebhookBody(array $overrides = []): array { $merchantKey = 'test-merchant-key'; $notification = array_merge([ 'appid' => 'APPID123', 'notify_time' => '1576842150', 'merch_code' => 'MERCH001', 'merch_order_id' => 'EVB-FIXTURE-001-1', 'mm_order_id' => '01001814070006560257', 'trans_currency' => 'MMK', 'total_amount' => '15000', 'trade_status' => 'PAY_SUCCESS', 'trans_end_time' => '1576834704', 'nonce_str' => '513ba55344ad44c8b69465aae66f7703', 'sign_type' => 'SHA256', ], $overrides); $notification['sign'] = KbzSignature::sign($notification, $merchantKey); return ['Request' => $notification]; } beforeEach(function () { config(['services.kbz.merchant_key' => 'test-merchant-key']); }); test('a validly signed kbz webhook is acknowledged with the literal success body', function () { Log::spy(); $this->postJson('/api/v1/webhooks/kbz_mini_app', signedKbzWebhookBody()) ->assertOk() ->assertSee('success'); Log::shouldHaveReceived('info')->once(); }); test('an invalidly signed kbz webhook is rejected with 400, never 500', function () { Log::spy(); $body = signedKbzWebhookBody(); $body['Request']['sign'] = 'tampered'; $this->postJson('/api/v1/webhooks/kbz_mini_app', $body) ->assertStatus(400); Log::shouldHaveReceived('warning')->once(); }); test('an unknown gateway in the route segment 404s rather than reaching a controller', function () { $this->postJson('/api/v1/webhooks/not_a_real_gateway', signedKbzWebhookBody()) ->assertNotFound(); }); test('a missing signature is rejected with 400', function () { $this->postJson('/api/v1/webhooks/kbz_mini_app', ['Request' => ['trade_status' => 'PAY_SUCCESS']]) ->assertStatus(400); }); test('the optional encrypted booking id segment is accepted and decrypted for logging', function () { Log::spy(); $booking = Booking::factory()->create(); $encrypted = Crypt::encryptString((string) $booking->id); $this->postJson("/api/v1/webhooks/kbz_mini_app/{$encrypted}", signedKbzWebhookBody()) ->assertOk(); Log::shouldHaveReceived('info')->withArgs( fn (string $message, array $context): bool => $context['booking_id'] === $booking->id )->once(); }); test('a garbage encrypted booking id segment does not fail the webhook', function () { $this->postJson('/api/v1/webhooks/kbz_mini_app/not-a-real-ciphertext', signedKbzWebhookBody()) ->assertOk(); }); test('no auth:sanctum is required to reach the webhook', function () { $this->postJson('/api/v1/webhooks/kbz_mini_app', signedKbzWebhookBody()) ->assertOk(); });