Connect AdSense over OAuth, sync entities and daily reports locally, and show placement performance in the admin panel without calling the Management API from public pages.
181 lines
7.1 KiB
PHP
181 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\SyncAdsenseJob;
|
|
use App\Models\AdsenseConnection;
|
|
use App\Models\User;
|
|
use App\Services\Adsense\AdsenseOAuthService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
config([
|
|
'adsense.client_id' => '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);
|
|
});
|