HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/tests/Feature/AdsR9_1ObservabilityDashboardTest.php
<?php

/**
 * Ads Phase R9.1 — Admin observability dashboard.
 *
 * Verifies:
 *   1. Route registered as `refactor.admin.ads.observability`.
 *   2. Controller renders the observability page for an admin user.
 *   3. Page surfaces the four banner stats (events / error rate /
 *      errors / open breakers).
 *   4. Per-platform breakdown table renders all 4 platforms even when
 *      they have no activity logs (zero-state row).
 *   5. AdActivityLog rows in the window populate the per-platform
 *      success / error counts.
 *   6. Recent errors timeline renders the latest 25 error rows.
 *   7. Top error actions card aggregates by (action, platform).
 *   8. Open circuit breakers card reads the decorator's cache state.
 *   9. Window selector preserves the selected value via query param.
 */

use App\Model\AdActivityLog;
use App\Model\AdPlatform;
use App\Model\Customer;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
// (DB removed — unused)

uses(Tests\TestCase::class);
uses(DatabaseTransactions::class);

beforeEach(function () {
    // Build a stub admin User per the F51 admin-test pattern — actingAs needs
    // an Authenticatable, and AdminController guards just check the User has
    // an `admin` relation present.  Skip cleanly if the test DB is empty.
    if (! Customer::orderBy('id')->first()) {
        $this->markTestSkipped('No seeded customer — run DemoSeeder first');
    }
    $this->adminUser = r91MakeAdminUser();
});

function r91MakeAdminUser(): \App\Model\User
{
    // Reuse the seeded admin row so the layout has all the fields it
    // expects (first_name/last_name/permissions/groups) rather than
    // a bare anonymous-class stub.
    $existingAdmin = \App\Model\Admin::orderBy('id')->first();
    $user = new \App\Model\User();
    $user->id = $existingAdmin?->id ?? 999_998;
    $user->email = $existingAdmin?->email ?? '[email protected]';
    $user->first_name = $existingAdmin?->first_name ?? 'Admin';
    $user->last_name = $existingAdmin?->last_name ?? 'User';
    $user->customer_id = null;
    if ($existingAdmin !== null) {
        $user->setRelation('admin', $existingAdmin);
    } else {
        $admin = new class extends \App\Model\Admin {
            public function getPermission($name) { return 'yes'; }
        };
        $admin->id = 999_998;
        $admin->email = $user->email;
        $admin->first_name = 'Admin';
        $admin->last_name = 'User';
        $user->setRelation('admin', $admin);
    }
    return $user;
}

function r91FactoryActivity(int $customerId, string $platform, string $status, string $action = 'campaign.created', ?\DateTimeInterface $when = null): AdActivityLog
{
    $log = new AdActivityLog();
    $log->customer_id = $customerId;
    $log->loggable_type = \App\Model\AdCampaign::class; // schema requires non-null
    $log->loggable_id = 0;
    $log->platform = $platform;
    $log->status = $status;
    $log->action = $action;
    $log->title = 'R9.1 test ' . $status;
    $log->error_message = $status === AdActivityLog::STATUS_ERROR ? 'Sample error message for R9.1' : null;
    $log->created_at = $when ?? now();
    $log->save();
    return $log;
}

// ---------------------------------------------------------------------------
// 1-2. Route + render
// ---------------------------------------------------------------------------

test('route registered and admin can access observability page', function () {
    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    $response->assertSee('Ads observability');
});

// ---------------------------------------------------------------------------
// 3. Banner stats rendered
// ---------------------------------------------------------------------------

test('banner surfaces the four key metrics', function () {
    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    $response->assertSee('Events');
    $response->assertSee('Error rate');
    $response->assertSee('Errors');
    $response->assertSee('Open breakers');
});

// ---------------------------------------------------------------------------
// 4. Zero-state platform breakdown table
// ---------------------------------------------------------------------------

test('per-platform breakdown table renders all 4 platforms even without activity', function () {
    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    foreach (AdPlatform::PLATFORMS as $platform) {
        $label = AdPlatform::PLATFORM_LABELS[$platform] ?? ucfirst($platform);
        $response->assertSee($label);
    }
});

// ---------------------------------------------------------------------------
// 5. AdActivityLog populates breakdown
// ---------------------------------------------------------------------------

test('AdActivityLog rows populate per-platform error counts', function () {
    $customer = Customer::orderBy('id')->first();
    if (! $customer) {
        $this->markTestSkipped('No seeded customer');
    }
    $customerId = $customer->local()->id;

    // 3 errors on meta + 1 success on google.
    r91FactoryActivity($customerId, 'meta', AdActivityLog::STATUS_ERROR);
    r91FactoryActivity($customerId, 'meta', AdActivityLog::STATUS_ERROR);
    r91FactoryActivity($customerId, 'meta', AdActivityLog::STATUS_ERROR);
    r91FactoryActivity($customerId, 'google', AdActivityLog::STATUS_SUCCESS);

    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    // Meta error rate at 100% (3/3) — should render the danger badge.
    // The number of errors should appear in the breakdown.
    expect(
        substr_count($response->getContent(), 'mc-badge-danger')
    )->toBeGreaterThan(0);
});

// ---------------------------------------------------------------------------
// 6. Recent errors timeline
// ---------------------------------------------------------------------------

test('recent errors timeline renders the latest error rows with title', function () {
    $customer = Customer::orderBy('id')->first();
    if (! $customer) {
        $this->markTestSkipped('No seeded customer');
    }

    $log = new AdActivityLog();
    $log->customer_id = $customer->local()->id;
    $log->loggable_type = \App\Model\AdCampaign::class;
    $log->loggable_id = 0;
    $log->platform = 'meta';
    $log->status = AdActivityLog::STATUS_ERROR;
    $log->action = 'campaign.publish';
    $log->title = 'R9.1 specific error title for assertion';
    $log->error_message = 'Token expired';
    $log->created_at = now();
    $log->save();

    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    $response->assertSee('R9.1 specific error title for assertion');
    $response->assertSee('campaign.publish');
});

// ---------------------------------------------------------------------------
// 7. Top error actions card
// ---------------------------------------------------------------------------

test('top error actions card aggregates by action+platform', function () {
    $customer = Customer::orderBy('id')->first();
    if (! $customer) {
        $this->markTestSkipped('No seeded customer');
    }
    $customerId = $customer->local()->id;

    // 5 same-action errors.
    for ($i = 0; $i < 5; $i++) {
        r91FactoryActivity($customerId, 'meta', AdActivityLog::STATUS_ERROR, 'r91.repeat.action');
    }
    // 2 different errors.
    r91FactoryActivity($customerId, 'google', AdActivityLog::STATUS_ERROR, 'r91.other.action');
    r91FactoryActivity($customerId, 'google', AdActivityLog::STATUS_ERROR, 'r91.other.action');

    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    $response->assertSee('r91.repeat.action');
    $response->assertSee('r91.other.action');
});

// ---------------------------------------------------------------------------
// 8. Circuit breaker card reads cache
// ---------------------------------------------------------------------------

test('open circuit breakers card surfaces cache-tracked state', function () {
    $customer = Customer::orderBy('id')->first();
    if (! $customer) {
        $this->markTestSkipped('No seeded customer');
    }
    $platform = new AdPlatform([
        'uid' => 'r91cb_' . uniqid(),
        'customer_id' => $customer->local()->id,
        'platform' => 'meta',
        'name' => 'R9.1 Circuit Test',
        'access_token' => 'r91tok',
        'platform_user_id' => 'p1',
        'platform_user_name' => 'r91',
    ]);
    $platform->status = AdPlatform::STATUS_ACTIVE;
    $platform->health_status = AdPlatform::HEALTH_HEALTHY;
    $platform->save();

    Cache::put("ads_cb:meta:{$platform->id}:state", 'open', 60);
    Cache::put("ads_cb:meta:{$platform->id}:failures", 7, 60);

    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability'));
    $response->assertOk();
    $response->assertSee('R9.1 Circuit Test');
    $response->assertSee('open');
    $response->assertSee('7 ');
});

// ---------------------------------------------------------------------------
// 9. Window selector
// ---------------------------------------------------------------------------

test('window selector preserves selection via query param', function () {
    $response = $this->actingAs($this->adminUser)->withoutMiddleware()->get(route('refactor.admin.ads.observability', ['window' => '7d']));
    $response->assertOk();
    // 7-day window resolves to 168 hours; banner desc renders that.
    $response->assertSee('168');
});