'test-client-id', 'adsense.client_secret' => 'test-client-secret', 'adsense.redirect_uri' => 'https://skinbase.org/admin/adsense/oauth/callback', 'adsense.sync_enabled' => true, 'adsense.retry_sleep_ms' => 0, ]); }); it('lets an admin start the oauth connection with readonly offline access', function (): void { $admin = User::factory()->create(['role' => 'admin']); $first = $this->actingAs($admin)->get(route('admin.adsense.connect')); $first->assertRedirect(); $location = (string) $first->headers->get('Location'); $state = session(AdsenseOAuthService::SESSION_STATE_KEY); expect($location)->toContain('https://accounts.google.com/o/oauth2/v2/auth') ->and($location)->toContain(urlencode('https://www.googleapis.com/auth/adsense.readonly')) ->and($location)->toContain('access_type=offline') ->and($location)->toContain('prompt=consent') ->and($location)->toContain('include_granted_scopes=true') ->and($location)->toContain('response_type=code') ->and($location)->toContain(urlencode('https://skinbase.org/admin/adsense/oauth/callback')) ->and($state)->toBeString() ->and(strlen((string) $state))->toBe(64); $second = $this->actingAs($admin)->get(route('admin.adsense.connect')); $secondState = session(AdsenseOAuthService::SESSION_STATE_KEY); expect($secondState)->not->toBe($state); }); it('rejects oauth start for guests and non-admins', function (): void { $this->get(route('admin.adsense.connect'))->assertRedirect(route('login')); $user = User::factory()->create(['role' => 'user']); $this->actingAs($user)->get(route('admin.adsense.connect'))->assertForbidden(); $manager = User::factory()->create(['role' => 'manager']); $this->actingAs($manager)->get(route('admin.adsense.connect'))->assertForbidden(); }); it('rejects invalid missing and denied oauth callbacks', function (): void { $admin = User::factory()->create(['role' => 'admin']); $this->actingAs($admin) ->withSession([AdsenseOAuthService::SESSION_STATE_KEY => 'valid-state']) ->get(route('admin.adsense.callback', ['code' => 'abc', 'state' => 'wrong-state'])) ->assertRedirect(route('admin.adsense.index')) ->assertSessionHas('error', 'Invalid OAuth state.'); $this->actingAs($admin) ->get(route('admin.adsense.callback', ['code' => 'abc', 'state' => 'anything'])) ->assertRedirect(route('admin.adsense.index')) ->assertSessionHas('error', 'Invalid OAuth state.'); $this->actingAs($admin) ->withSession([AdsenseOAuthService::SESSION_STATE_KEY => 'valid-state']) ->get(route('admin.adsense.callback', ['state' => 'valid-state'])) ->assertRedirect(route('admin.adsense.index')) ->assertSessionHas('error', 'Missing authorization code.'); $this->actingAs($admin) ->withSession([AdsenseOAuthService::SESSION_STATE_KEY => 'valid-state']) ->get(route('admin.adsense.callback', [ 'state' => 'valid-state', 'error' => 'access_denied', 'error_description' => 'The user denied access', ])) ->assertRedirect(route('admin.adsense.index')); }); it('exchanges the authorization code and stores an encrypted refresh token', function (): void { Queue::fake(); Http::fake([ 'https://oauth2.googleapis.com/token' => Http::response([ 'access_token' => 'access-secret', 'refresh_token' => 'refresh-secret', 'expires_in' => 3600, 'token_type' => 'Bearer', ]), 'https://adsense.googleapis.com/v2/accounts*' => Http::response([ 'accounts' => [[ 'name' => 'accounts/pub-1234567890', 'displayName' => 'Skinbase', ]], ]), ]); $admin = User::factory()->create(['role' => 'admin']); $response = $this->actingAs($admin) ->withSession([AdsenseOAuthService::SESSION_STATE_KEY => 'valid-state']) ->get(route('admin.adsense.callback', [ 'state' => 'valid-state', 'code' => 'auth-code-secret', ])); $response->assertRedirect(route('admin.adsense.index')) ->assertSessionHas('success'); $connection = AdsenseConnection::current(); expect($connection)->not->toBeNull() ->and($connection->encrypted_refresh_token)->toBe('refresh-secret') ->and($connection->account_resource_name)->toBe('accounts/pub-1234567890') ->and($connection->status)->toBe(AdsenseConnection::STATUS_CONNECTED); $raw = DB::table('adsense_connections')->value('encrypted_refresh_token'); expect($raw)->not->toBe('refresh-secret') ->and($raw)->not->toContain('refresh-secret'); $this->actingAs($admin) ->get(route('admin.adsense.index')) ->assertOk() ->assertDontSee('refresh-secret') ->assertDontSee('access-secret') ->assertDontSee('auth-code-secret') ->assertDontSee('test-client-secret') ->assertInertia(fn (Assert $page) => $page ->component('Admin/Adsense/Index') ->missing('connection.encrypted_refresh_token') ->where('connection.status', 'connected')); Queue::assertPushed(SyncAdsenseJob::class); }); it('asks the administrator to choose when multiple adsense accounts exist', function (): void { Queue::fake(); Http::fake([ 'https://oauth2.googleapis.com/token' => Http::response([ 'access_token' => 'access-secret', 'refresh_token' => 'refresh-secret', 'expires_in' => 3600, ]), 'https://adsense.googleapis.com/v2/accounts*' => Http::response([ 'accounts' => [ ['name' => 'accounts/pub-1', 'displayName' => 'Skinbase'], ['name' => 'accounts/pub-2', 'displayName' => 'Other'], ], ]), ]); $admin = User::factory()->create(['role' => 'admin']); $this->actingAs($admin) ->withSession([AdsenseOAuthService::SESSION_STATE_KEY => 'valid-state']) ->get(route('admin.adsense.callback', [ 'state' => 'valid-state', 'code' => 'auth-code-secret', ])) ->assertRedirect(route('admin.adsense.index')); expect(session(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY))->toHaveCount(2); Queue::assertNothingPushed(); $this->actingAs($admin) ->post(route('admin.adsense.account'), ['account_resource_name' => 'accounts/pub-1']) ->assertRedirect(route('admin.adsense.index')); expect(AdsenseConnection::current()?->account_resource_name)->toBe('accounts/pub-1'); Queue::assertPushed(SyncAdsenseJob::class); });