'test-client-id', 'adsense.client_secret' => 'test-client-secret', 'adsense.redirect_uri' => 'https://skinbase.org/admin/adsense/oauth/callback', 'adsense.sync_enabled' => true, ]); }); function dashboardConnection(): AdsenseConnection { $connection = new AdsenseConnection; $connection->account_resource_name = 'accounts/pub-123'; $connection->account_display_name = 'Skinbase'; $connection->status = AdsenseConnection::STATUS_CONNECTED; $connection->connected_at = now()->subHour(); $connection->last_successful_sync_at = now()->subMinutes(12); $connection->encrypted_refresh_token = 'refresh-secret'; $connection->save(); return $connection; } function seedStat(string $date, string $type, string $key, array $metrics, ?string $label = null): void { AdsenseDailyStat::query()->create(array_merge([ 'date' => $date, 'account_resource_name' => 'accounts/pub-123', 'dimension_type' => $type, 'dimension_key' => $key, 'dimension_label' => $label ?? $key, 'currency_code' => 'EUR', 'last_synced_at' => now(), ], $metrics)); } it('requires an admin for the adsense dashboard', function (): void { $this->get(route('admin.adsense.index'))->assertRedirect(route('login')); $user = User::factory()->create(['role' => 'user']); $this->actingAs($user)->get(route('admin.adsense.index'))->assertForbidden(); }); it('renders connection status and period filters from local statistics', function (): void { Http::fake(); $admin = User::factory()->create(['role' => 'admin']); dashboardConnection(); AdsenseEntity::query()->create([ 'account_resource_name' => 'accounts/pub-123', 'type' => AdsenseEntity::TYPE_AD_UNIT, 'reporting_dimension_id' => '111', 'display_name' => 'Skinbase_Artwork_BeforeComments', 'last_seen_at' => now(), ]); seedStat(now()->toDateString(), 'total', '__total__', [ 'page_views' => 100, 'impressions' => 80, 'clicks' => 4, 'estimated_earnings' => '5.00', 'ad_requests' => 90, 'matched_ad_requests' => 80, ]); seedStat(now()->subDays(7)->toDateString(), 'total', '__total__', [ 'page_views' => 50, 'impressions' => 40, 'clicks' => 1, 'estimated_earnings' => '1.00', 'ad_requests' => 45, 'matched_ad_requests' => 40, ]); seedStat(now()->toDateString(), 'ad_unit', '111', [ 'page_views' => 100, 'impressions' => 80, 'clicks' => 4, 'estimated_earnings' => '5.00', ], 'Skinbase_Artwork_BeforeComments'); seedStat(now()->toDateString(), 'platform', 'DESKTOP', [ 'page_views' => 60, 'impressions' => 50, 'clicks' => 3, 'estimated_earnings' => '3.00', ]); seedStat(now()->toDateString(), 'country', 'SI', [ 'page_views' => 20, 'impressions' => 18, 'clicks' => 2, 'estimated_earnings' => '1.50', ]); $this->actingAs($admin) ->get(route('admin.adsense.index', ['period' => 'today'])) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('Admin/Adsense/Index') ->where('connection.status', 'connected') ->where('range.period', 'today') ->where('dashboard.placements.0.label', 'Skinbase_Artwork_BeforeComments') ->where('dashboard.kpis.0.key', 'estimated_earnings') ->missing('connection.encrypted_refresh_token')); $this->actingAs($admin) ->get(route('admin.adsense.index', ['period' => 'yesterday'])) ->assertOk() ->assertInertia(fn (Assert $page) => $page->where('range.period', 'yesterday')); $this->actingAs($admin) ->get(route('admin.adsense.index', ['period' => '7d'])) ->assertOk() ->assertInertia(fn (Assert $page) => $page->where('range.period', '7d')); $this->actingAs($admin) ->get(route('admin.adsense.index', ['period' => '30d'])) ->assertOk() ->assertInertia(fn (Assert $page) => $page->where('range.period', '30d')); $from = now()->subDays(2)->toDateString(); $to = now()->toDateString(); $this->actingAs($admin) ->get(route('admin.adsense.index', ['period' => 'custom', 'from' => $from, 'to' => $to])) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->where('range.period', 'custom') ->where('range.from', $from) ->where('range.to', $to)); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'adsense.googleapis.com') || str_contains($request->url(), 'oauth2.googleapis.com')); }); it('authorizes manual sync and keeps historical statistics after disconnect', function (): void { Queue::fake(); $admin = User::factory()->create(['role' => 'admin']); $connection = dashboardConnection(); seedStat(now()->toDateString(), 'total', '__total__', [ 'page_views' => 10, 'clicks' => 1, 'estimated_earnings' => '2.00', ]); $user = User::factory()->create(['role' => 'user']); $this->actingAs($user)->post(route('admin.adsense.sync'))->assertForbidden(); $this->actingAs($user)->delete(route('admin.adsense.disconnect'))->assertForbidden(); $this->actingAs($admin) ->post(route('admin.adsense.sync')) ->assertRedirect(route('admin.adsense.index')); Queue::assertPushed(SyncAdsenseJob::class); Http::fake([ 'https://oauth2.googleapis.com/revoke' => Http::response([], 200), ]); $this->actingAs($admin) ->delete(route('admin.adsense.disconnect')) ->assertRedirect(route('admin.adsense.index')); expect($connection->fresh()->encrypted_refresh_token)->toBeNull() ->and($connection->fresh()->status)->toBe(AdsenseConnection::STATUS_DISCONNECTED) ->and(AdsenseDailyStat::query()->count())->toBe(1); }); it('registers hourly and nightly adsense scheduler entries', function (): void { Artisan::call('schedule:list'); $output = Artisan::output(); expect($output)->toContain('adsense:sync --days=3') ->and($output)->toContain('adsense:sync --days=30'); }); it('does not call the adsense management api from the public homepage', function (): void { Http::fake(); $this->get('/')->assertOk(); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'adsense.googleapis.com') || str_contains($request->url(), 'oauth2.googleapis.com')); });