Testing

The client sends every request with Laravel’s HTTP client, so Http::fake() sees them all. Never let a test reach Supplydrive: call Http::preventStrayRequests(), and a forgotten fake fails the test instead of logging in with a real account.

The two endpoints

use Illuminate\Support\Facades\Http;

Http::preventStrayRequests();

Http::fake([
    // The login. Any access_token will do; expires_in decides how long it is cached.
    'portal.supplydrive.com/realms/*' => Http::response([
        'access_token' => 'test-token',
        'token_type' => 'Bearer',
        'expires_in' => 300,
    ]),

    // The status endpoint: a list of items as Supplydrive returns them.
    'portal.supplydrive.com/SD/*' => Http::response([
        ['id' => 1, 'functionalId' => 'F260001', 'status' => 'Afgeleverd', 'interfaceId' => 1000],
    ]),
]);

The client needs an account, or the first call throws before anything is sent:

config(['supplydrive.username' => 'test', 'supplydrive.password' => 'secret']);

Keep the test cache on the array store (the default of a Laravel test run), or a token cached by one test is reused by the next.

The send endpoint

Http::fake([
    'rest.supplydrive.com/*' => Http::response(['berichtId' => 4711]),
]);

config(['supplydrive.send.username' => 'test', 'supplydrive.send.password' => 'secret']);

Fake a 401 to see your code handle a refused account, a 422 for a refused document, and a body without an id (Http::response('<Ack/>')) to see that your code stores nothing but still marks the invoice as sent. For a timeout, throw from the fake: Http::fake(fn () => throw new \Illuminate\Http\Client\ConnectionException('timed out')). Assert that exactly one request was sent (Http::assertSentCount(1)); a send that is repeated delivers the invoice twice.

A complete test

For the peppol:check-deliveries command of the quick start:

<?php

use App\Models\Invoice;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

beforeEach(function () {
    Http::preventStrayRequests();
    config(['supplydrive.username' => 'test', 'supplydrive.password' => 'secret']);
});

it('records the delivery of an invoice Supplydrive delivered', function () {
    $invoice = Invoice::factory()->create(['invoice_nr' => 'F260001', 'peppol_sent_at' => now()]);

    Http::fake([
        'portal.supplydrive.com/realms/*' => Http::response(['access_token' => 'test-token', 'expires_in' => 300]),
        'portal.supplydrive.com/SD/*' => Http::response([
            ['id' => 1, 'functionalId' => 'F260001', 'status' => 'Afgeleverd', 'interfaceId' => 1000],
        ]),
    ]);

    $this->artisan('peppol:check-deliveries')->assertSuccessful();

    expect($invoice->fresh()->peppol_delivered_at)->not->toBeNull();

    Http::assertSent(fn (Request $request) => str_ends_with($request->url(), '/SD/Berichten/Status?functioneelId=F260001')
        && $request->hasHeader('Authorization', 'Bearer test-token'));
});

it('leaves an invoice alone that Supplydrive does not know', function () {
    $invoice = Invoice::factory()->create(['invoice_nr' => 'F260002', 'peppol_sent_at' => now()]);

    Http::fake([
        'portal.supplydrive.com/realms/*' => Http::response(['access_token' => 'test-token', 'expires_in' => 300]),
        'portal.supplydrive.com/SD/*' => Http::response(['message' => 'not found'], 404),
    ]);

    $this->artisan('peppol:check-deliveries')->assertSuccessful();

    expect($invoice->fresh()->peppol_delivered_at)->toBeNull();
});

it('stops when Supplydrive refuses the account', function () {
    Invoice::factory()->create(['invoice_nr' => 'F260003', 'peppol_sent_at' => now()]);

    Http::fake([
        'portal.supplydrive.com/realms/*' => Http::response(['error' => 'invalid_grant'], 401),
    ]);

    $this->artisan('peppol:check-deliveries')->assertFailed();
});

Faking the client instead

When the code under test only needs messages, skip the HTTP layer and bind a fake client:

use Darvis\SupplydriveApi\Message;
use Darvis\SupplydriveApi\SupplydriveClient;

$this->mock(SupplydriveClient::class, function ($mock) {
    $mock->shouldReceive('byFunctionalId')
        ->with('F260001')
        ->andReturn([Message::fromArray(['id' => 1, 'functionalId' => 'F260001', 'status' => 'Afgeleverd', 'interfaceId' => 1000])]);
});

Message::fromArray() takes the same array the API returns, so a fixture reads like a response.

Things worth a test of their own

  • A 401 for a status lookup is thrown after one repeat; fake the status endpoint with a sequence (Http::sequence()) to see the repeat.
  • An unknown status is pending: ['status' => 'Something new'] must not mark an invoice as delivered or failed.
  • A timeout is an Illuminate\Http\Client\ConnectionException: Http::fake(fn () => throw new ConnectionException('timed out')).