Google - v1.0.0

Testing

The package ships with a Pest test suite you can use as a reference. This page collects patterns for testing your own code against artisanpack-ui/google.

Test bootstrap

The package's own tests use Orchestra Testbench with a base TestCase in tests/. To wire up an app-level test that talks to the package:

use ArtisanPackUI\Google\GoogleServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    protected function getPackageProviders( $app ): array
    {
        return [ GoogleServiceProvider::class ];
    }

    protected function defineDatabaseMigrations(): void
    {
        $this->loadMigrationsFrom( __DIR__ . '/../vendor/artisanpack-ui/google/database/migrations' );
    }
}

Or use the RefreshDatabase trait in a normal Laravel TestCase — the package registers its migrations via $this->loadMigrationsFrom() in boot(), so php artisan migrate in the test DB is enough.

Testing OAuth flows

Fake Google's endpoints:

use Illuminate\Support\Facades\Http;

Http::fake( [
    'https://oauth2.googleapis.com/token' => Http::sequence()
        ->push( [
            'access_token'  => 'initial-access-token',
            'refresh_token' => 'a-refresh-token',
            'expires_in'    => 3600,
            'token_type'    => 'Bearer',
            'scope'         => 'openid https://www.googleapis.com/auth/userinfo.email',
            'id_token'      => makeIdToken( sub: 'g-123', email: 'you@example.com' ),
        ] ),
] );

Where makeIdToken() builds a minimal id_token payload (base64url of {header}.{payload}.{signature} — the signature can be dummy since the package doesn't verify it):

function makeIdToken( string $sub, string $email ): string
{
    $header  = base64url( json_encode( [ 'alg' => 'RS256', 'typ' => 'JWT' ] ) );
    $payload = base64url( json_encode( [ 'sub' => $sub, 'email' => $email ] ) );

    return "{$header}.{$payload}.signature";
}

function base64url( string $raw ): string
{
    return rtrim( strtr( base64_encode( $raw ), '+/', '-_' ), '=' );
}

Then drive the flow:

use ArtisanPackUI\Google\Facades\Google;
use ArtisanPackUI\Google\Models\GoogleConnection;

$user = User::factory()->create();

// Pretend the /connect handler ran.
$url = Google::oauth()->authorizationUrl( $user->id );

// Extract state from the session; simulate the callback.
$state = session( 'google.oauth.state' );

Google::oauth()->handleCallback( 'test-auth-code', $state );

$connection = GoogleConnection::firstWhere( 'user_id', $user->id );

expect( $connection )->not->toBeNull();
expect( $connection->email )->toBe( 'you@example.com' );
expect( $connection->access_token )->toBe( 'initial-access-token' );

Testing the token manager

Same pattern — fake the token endpoint and assert the manager returns the fresh token:

use ArtisanPackUI\Google\Facades\Google;

Http::fake( [
    'https://oauth2.googleapis.com/token' => Http::response( [
        'access_token' => 'refreshed',
        'expires_in'   => 3600,
        'token_type'   => 'Bearer',
    ] ),
] );

$connection = GoogleConnection::factory()->create( [
    'access_token'  => 'expired-token',
    'refresh_token' => 'a-refresh-token',
    'expires_at'    => now()->subMinute(),
    'status'        => 'connected',
] );

expect( Google::tokens()->getValidAccessToken( $connection ) )->toBe( 'refreshed' );
expect( $connection->fresh()->access_token )->toBe( 'refreshed' );

Test the invalid_grant disconnect:

Http::fake( [
    'https://oauth2.googleapis.com/token' => Http::response( [
        'error' => 'invalid_grant',
    ], 400 ),
] );

expect( fn () => Google::tokens()->refresh( $connection ) )
    ->toThrow( TokenRefreshException::class );

expect( $connection->fresh()->status )->toBe( 'disconnected' );
expect( $connection->fresh()->disconnect_reason )->toBe( 'Refresh token revoked or expired.' );

Testing scope contributions

Register a scope inside the test and assert all() picks it up:

use ArtisanPackUI\Google\Facades\Google;
use ArtisanPackUI\Hooks\Facades\Filter;

Filter::add( 'ap.google.scopes', function ( array $scopes ): array {
    $scopes[] = 'https://www.googleapis.com/auth/analytics.readonly';
    return $scopes;
} );

expect( Google::scopes()->all() )->toContain( 'https://www.googleapis.com/auth/analytics.readonly' );

Filter registrations persist across tests within the same process — call Filter::remove() in tearDown() or use a fresh filter registry per test if you need isolation.

Testing configuration drivers

Switch drivers in the test's setUp():

config( [ 'google.driver' => 'database' ] );

Google::config()->save( [
    'client_id'     => 'test-client-id',
    'client_secret' => 'test-client-secret',
    'redirect_uri'  => 'https://tests.test/google/auth/callback',
] );

expect( Google::config()->isConfigured() )->toBeTrue();
expect( Google::config()->getClientSecret() )->toBe( 'test-client-secret' );

Because ConfigurationRepository is bind()ed (not singleton()), the container re-reads config('google.driver') on each resolve — you can flip the driver mid-test without container-flushing.

Testing controllers

$user = User::factory()->create();

$this->actingAs( $user )
    ->get( route( 'google.auth.connect' ) )
    ->assertRedirectContains( 'https://accounts.google.com/o/oauth2/v2/auth' );

For the callback, prime the session first:

session( [
    'google.oauth.state'    => 'test-state',
    'google.oauth.verifier' => 'test-verifier',
    'google.oauth.user_id'  => $user->id,
] );

Http::fake( [ 'https://oauth2.googleapis.com/token' => Http::response( [ /* ... */ ] ) ] );

$this->actingAs( $user )
    ->get( route( 'google.auth.callback' ) . '?code=test-code&state=test-state' )
    ->assertRedirect( '/' );

$this->assertDatabaseHas( 'google_connections', [
    'user_id' => $user->id,
    'status'  => 'connected',
] );

Testing the Livewire component

use ArtisanPackUI\Google\Livewire\ConnectionManager;
use Livewire\Livewire;

Livewire::actingAs( $user )
    ->test( ConnectionManager::class )
    ->assertSee( 'Connect Google' );

// Seed a connection and re-test:
GoogleConnection::factory()->for( $user )->create( [ 'email' => 'you@example.com' ] );

Livewire::actingAs( $user )
    ->test( ConnectionManager::class )
    ->assertSee( 'you@example.com' );

Testing the status endpoint

$response = $this->actingAs( $user )->getJson( '/google/auth/status' );

$response->assertOk()->assertJson( [
    'connected' => false,
    'email'     => null,
] );

Testing the JS components

See Connection UI/React#Testing and Connection UI/Vue#Testing.

Running the package's own tests

From the package directory:

composer test          # runs Pest
composer lint          # php-cs-fixer --dry-run + phpcs
composer fix           # php-cs-fixer fix

From this dev app (with the package symlinked):

php artisan test --compact