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.
196 lines
6.9 KiB
PHP
196 lines
6.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\SyncAdsenseJob;
|
|
use App\Models\AdsenseConnection;
|
|
use App\Models\AdsenseDailyStat;
|
|
use App\Models\AdsenseEntity;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
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,
|
|
]);
|
|
});
|
|
|
|
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'));
|
|
});
|