# Testing

Your tests should never call PayMongo. `Paymongo::fake()` answers every request the package makes with a realistic response, records what was sent so you can assert on it, and lets you stub any response, errors included. `Luigel\Paymongo\Testing\Fixtures` builds the same payloads for your own stubs and for webhook events.

The examples on this page test the app from [Your first payment](/your-first-payment.md): a `POST /orders/{order}/checkout` route that creates a checkout session, and a `FulfillOrder` listener that marks the order paid.

## Fake PayMongo

Call `Paymongo::fake()` before the code under test runs, then assert on what it sent:

```php
use App\Models\Order;
use Illuminate\Http\Client\Request;
use Luigel\Paymongo\Facades\Paymongo;
use Luigel\Paymongo\Testing\Fixtures;

it('sends the customer to PayMongo to pay for their order', function () {
    Paymongo::fake([
        '*/checkout_sessions' => Fixtures::checkoutSession(['checkout_url' => 'https://checkout.paymongo.com/cs_test']),
    ]);

    $order = Order::create(['reference' => 'ORDER-1234', 'description' => 'Leather wallet', 'amount' => 150050]);

    $this->post(route('orders.checkout', $order))
        ->assertRedirect('https://checkout.paymongo.com/cs_test');

    Paymongo::assertSent(fn (Request $request): bool => str_ends_with($request->url(), '/checkout_sessions')
        && $request['data']['attributes']['reference_number'] === 'ORDER-1234'
        && $request['data']['attributes']['line_items'][0]['amount'] === 150050);
});

it('sends nothing to PayMongo for an order that does not exist', function () {
    Paymongo::fake();

    $this->post('/orders/999/checkout')->assertNotFound();

    Paymongo::assertNothingSent();
});
```

With no stubs, the fake answers every endpoint of PayMongo's API:

- a **create** returns the resource with the attributes you sent;
- a **retrieve** returns the resource with the id you asked for;
- a **list** returns a page of one;
- an **action** such as attach, capture, cancel, expire, archive, or enable returns the resource it acted on.

A request to an endpoint the fake does not know gets a `404`, which the package throws as a `ResourceNotFoundException` whose message names the endpoint. Stub that endpoint to handle it.

`Paymongo::assertSent()` passes when your callback returns `true` for one of the requests sent. The callback receives Laravel's `Illuminate\Http\Client\Request`: read the URL with `$request->url()`, the body with `$request['data']['attributes']`, and headers with `$request->hasHeader('Idempotency-Key')`. `Paymongo::assertNothingSent()` fails if any request was sent.

## Stub a response

Pass `Paymongo::fake()` URL patterns mapped to responses, as you would to `Http::fake()`. Your stubs win over the fake's own answers, so you can return a particular resource, a page, or an error. An error response throws the exception the package would throw for it (see [Errors](/errors.md)):

```php
use App\Models\Order;
use Illuminate\Support\Facades\Http;
use Luigel\Paymongo\Exceptions\InvalidRequestException;
use Luigel\Paymongo\Facades\Paymongo;

it('fails the checkout when PayMongo rejects the amount', function () {
    Paymongo::fake([
        '*/checkout_sessions' => Http::response([
            'errors' => [['code' => 'parameter_below_minimum', 'detail' => 'The minimum value for the amount is 2000.']],
        ], 400),
    ]);

    $order = Order::create(['reference' => 'ORDER-1235', 'description' => 'Sticker', 'amount' => 1000]);

    $this->withoutExceptionHandling();

    expect(fn () => $this->post(route('orders.checkout', $order)))
        ->toThrow(InvalidRequestException::class, 'The minimum value for the amount is 2000.');
});
```

Patterns match the whole URL, so start them with `*/`: `'*/checkout_sessions'`, `'*/payment_intents/pi_failing*'`. Stub a list with `Fixtures::list([Fixtures::payment(['amount' => 150050])])`.

## Fixtures

Every factory on `Fixtures` returns a response body, `{"data": ...}`, and takes an array of attribute overrides, including `id`. They are the same payloads the fake answers with.

| Factory | Builds |
|:--------|:-------|
| `paymentIntent()`, `paymentMethod()`, `payment()`, `refund()` | Payment intents, methods, payments, refunds |
| `checkoutSession()`, `link()`, `paymentLink()` | Checkout sessions, Classic Links, Payment Links |
| `mpmQr()`, `qrExecution()`, `staticQr()` | QR Ph codes |
| `customer()`, `customerPaymentMethod()`, `plan()`, `subscription()` | Customers and subscriptions |
| `payout()`, `payoutTransaction()`, `payoutSchedule()` | Payouts |
| `webhook()`, `source()` | Webhook endpoints, legacy sources |
| `event($type, $resource, $overrides)` | A webhook event about `$resource`, as PayMongo posts it |
| `list($items, $hasMore)` | A list response, as most list endpoints return |
| `flatList($items, $hasMore)` | A page of Payment Links |
| `payoutList($items, $nextCursor)` | A page of payouts or payout transactions |

`paymentLink()`, `mpmQr()` and `qrExecution()` build the flat shape those APIs return, with fields directly on `data`, and their overrides replace those fields. The rest nest them under `data.attributes`.

## Test a webhook

Post an event built with `Fixtures::event()` to your webhook route, signed with your endpoint's secret the way PayMongo signs it. That runs everything a real delivery does: the signature check, deduplication, and your listeners. To test just the listener, call it with the event object:

```php
use App\Listeners\FulfillOrder;
use App\Models\Order;
use Luigel\Paymongo\Events\CheckoutSessionPaymentPaid;
use Luigel\Paymongo\Testing\Fixtures;
use Luigel\Paymongo\Webhooks\WebhookEvent;

it('marks the order paid when PayMongo delivers checkout_session.payment.paid', function () {
    $order = Order::create(['reference' => 'ORDER-1234', 'description' => 'Leather wallet', 'amount' => 150050]);

    $payload = json_encode(Fixtures::event('checkout_session.payment.paid', Fixtures::checkoutSession([
        'reference_number' => 'ORDER-1234',
    ])));

    // Sign it the way PayMongo does, with your endpoint's secret.
    $timestamp = time();
    $signature = hash_hmac('sha256', "{$timestamp}.{$payload}", config('paymongo.webhooks.secret'));

    $this->call('POST', '/paymongo/webhook', server: [
        'CONTENT_TYPE' => 'application/json',
        'HTTP_PAYMONGO_SIGNATURE' => "t={$timestamp},te={$signature},li=",
    ], content: $payload)->assertOk();

    expect($order->fresh()->paid_at)->not->toBeNull();
});

it('fulfils the order in the listener', function () {
    $order = Order::create(['reference' => 'ORDER-1234', 'description' => 'Leather wallet', 'amount' => 150050]);

    $payload = Fixtures::event('checkout_session.payment.paid', Fixtures::checkoutSession([
        'reference_number' => 'ORDER-1234',
    ]));

    (new FulfillOrder)->handle(new CheckoutSessionPaymentPaid(WebhookEvent::fromArray($payload)));

    expect($order->fresh()->paid_at)->not->toBeNull();
});
```

With the `array` cache store, which Laravel's default `phpunit.xml` sets, each test starts with an empty cache, so deduplication does not carry over from one test to the next. Within one test, a second post of the same event is dropped: pass another id, `Fixtures::event('payment.paid', $payment, ['id' => 'evt_2'])`, to send a different event.

## Use Laravel's HTTP fakes

`Paymongo::fake()` is built on Laravel's `Http::fake()`, so the rest of Laravel's HTTP testing tools see the same requests: `Http::assertSent()`, `Http::assertSentCount()`, `Http::assertNotSent()`, and `Http::preventStrayRequests()`, which fails any request nothing is faking:

```php
use App\Models\Order;
use Illuminate\Support\Facades\Http;
use Luigel\Paymongo\Facades\Paymongo;

it('makes exactly one PayMongo request per checkout, and no other HTTP call', function () {
    Http::preventStrayRequests();
    Paymongo::fake();

    $order = Order::create(['reference' => 'ORDER-1234', 'description' => 'Leather wallet', 'amount' => 150050]);

    $this->post(route('orders.checkout', $order))->assertRedirect();

    Http::assertSentCount(1);
});
```

You can skip `Paymongo::fake()` and stub PayMongo with `Http::fake()` alone, using `Fixtures` for the bodies.
