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/be.naniguide.com/tests/Feature/AdsR9TelemetryTest.php
<?php

/**
 * Ads Phase R9 — TelemetryDecorator structured logging test suite.
 *
 * Closes gates O-1..O-10 (observability — trace_id, per-method duration,
 * sampled success, always-on error logging, structured payload shape).
 *
 * Verifies:
 *   1. Success path sampled per `ads.observability.sample_rate_success`
 *   2. Error path always logged at error level (rate=1.0 default)
 *   3. Log payload includes trace_id, platform, method, duration_ms,
 *      status, customer_id, ad_platform_id
 *   4. Error payload adds error_class + error_code + error_message
 *   5. trace_id is a UUID (36 chars) unique per call
 *   6. rate_success=0 never samples; rate=1 always samples
 *   7. Generic (non-AdPlatformException) throws still log at error level
 *   8. Every network-hitting method is wrapped (exists as override)
 */

use App\Model\AdPlatform;
use App\Services\Ads\AdPlatformAdapter;
use App\Services\Ads\AdPlatformException;
use App\Services\Ads\Decorators\TelemetryDecorator;
use App\Services\Ads\Exceptions\NetworkException;
use App\Services\Ads\MockAdPlatform;
use App\Services\Ads\Results\PublishResult;
use Illuminate\Support\Facades\Log;

uses(Tests\TestCase::class);

beforeEach(function () {
    config([
        'ads.observability.log_channel' => 'stack',
        'ads.observability.sample_rate_success' => 1.0, // always log in tests
        'ads.observability.sample_rate_error' => 1.0,
    ]);
});

function r9MakePlatform(): AdPlatform
{
    $p = new AdPlatform([
        'uid' => 'r9p_' . uniqid(),
        'customer_id' => 42,
        'platform' => 'mock',
        'name' => 'Mock',
    ]);
    $p->id = 77;
    return $p;
}

/** Adapter that succeeds. */
function r9Successful(): AdPlatformAdapter
{
    return new class extends MockAdPlatform {
        public function pauseCampaign(string $t, string $rcId, string $k): PublishResult
        {
            return new PublishResult(status: PublishResult::STATUS_PAUSED, remoteCampaignId: $rcId);
        }
    };
}

/** Adapter that throws NetworkException (an AdPlatformException subclass). */
function r9Network(): AdPlatformAdapter
{
    return new class extends MockAdPlatform {
        public function pauseCampaign(string $t, string $rcId, string $k): PublishResult
        {
            throw new NetworkException(message: 'downstream exploded', platformCode: 'TEST_NETWORK');
        }
    };
}

/** Adapter that throws a non-AdPlatformException (programmer error). */
function r9Generic(): AdPlatformAdapter
{
    return new class extends MockAdPlatform {
        public function pauseCampaign(string $t, string $rcId, string $k): PublishResult
        {
            throw new \RuntimeException('boom');
        }
    };
}

// ============================================================
// Success path — sampled per rate
// ============================================================

test('success path logs at info level with full payload', function () {
    $hits = 0;
    Log::partialMock()->shouldReceive('channel')->with('stack')->andReturnUsing(function () use (&$hits) {
        return new class($hits) {
            public function __construct(public &$h) {}
            public function info(string $m, array $c): void { $this->h++; }
            public function error(string $m, array $c): void { $this->h++; }
            public function warning(string $m, array $c): void { $this->h++; }
        };
    });
    $dec = new TelemetryDecorator(r9Successful(), r9MakePlatform());

    $dec->pauseCampaign('t', 'rc_abc', 'k1');

    expect($hits)->toBe(1);
});

test('success path includes trace_id + duration_ms + platform + method + customer_id', function () {
    $captured = null;
    Log::partialMock()
        ->shouldReceive('channel')
        ->andReturnUsing(function () use (&$captured) {
            return new class($captured) {
                public function __construct(public &$cap) {}
                public function info(string $msg, array $ctx): void { $this->cap = ['level' => 'info', 'msg' => $msg, 'ctx' => $ctx]; }
                public function error(string $msg, array $ctx): void { $this->cap = ['level' => 'error', 'msg' => $msg, 'ctx' => $ctx]; }
                public function warning(string $msg, array $ctx): void { $this->cap = ['level' => 'warning', 'msg' => $msg, 'ctx' => $ctx]; }
            };
        });

    $dec = new TelemetryDecorator(r9Successful(), r9MakePlatform());
    $dec->pauseCampaign('t', 'rc_abc', 'k1');

    expect($captured)->not->toBeNull()
        ->and($captured['level'])->toBe('info')
        ->and($captured['msg'])->toBe('ads.adapter.call')
        ->and($captured['ctx'])->toHaveKeys([
            'trace_id', 'platform', 'method', 'duration_ms',
            'status', 'customer_id', 'ad_platform_id',
        ])
        ->and($captured['ctx']['status'])->toBe('success')
        ->and($captured['ctx']['platform'])->toBe('mock')
        ->and($captured['ctx']['method'])->toBe('pauseCampaign')
        ->and($captured['ctx']['customer_id'])->toBe(42)
        ->and($captured['ctx']['ad_platform_id'])->toBe(77)
        ->and(strlen($captured['ctx']['trace_id']))->toBe(36); // UUID
});

test('sample_rate_success=0 never logs success', function () {
    config(['ads.observability.sample_rate_success' => 0.0]);
    $hits = 0;
    Log::partialMock()->shouldReceive('channel')->andReturnUsing(function () use (&$hits) {
        return new class($hits) {
            public function __construct(public &$h) {}
            public function info(string $m, array $c): void { $this->h++; }
            public function error(string $m, array $c): void { $this->h++; }
            public function warning(string $m, array $c): void { $this->h++; }
        };
    });

    $dec = new TelemetryDecorator(r9Successful(), r9MakePlatform());
    for ($i = 0; $i < 20; $i++) {
        $dec->pauseCampaign('t', 'rc', "k{$i}");
    }
    expect($hits)->toBe(0);
});

test('sample_rate_success=1 always logs', function () {
    config(['ads.observability.sample_rate_success' => 1.0]);
    $hits = 0;
    Log::partialMock()->shouldReceive('channel')->andReturnUsing(function () use (&$hits) {
        return new class($hits) {
            public function __construct(public &$h) {}
            public function info(string $m, array $c): void { $this->h++; }
            public function error(string $m, array $c): void { $this->h++; }
            public function warning(string $m, array $c): void { $this->h++; }
        };
    });

    $dec = new TelemetryDecorator(r9Successful(), r9MakePlatform());
    for ($i = 0; $i < 5; $i++) {
        $dec->pauseCampaign('t', 'rc', "k{$i}");
    }
    expect($hits)->toBe(5);
});

// ============================================================
// Error path — always logged (rate 1.0 default)
// ============================================================

test('AdPlatformException path logs at error level with error_class + error_code + error_message', function () {
    $captured = null;
    Log::partialMock()->shouldReceive('channel')->andReturnUsing(function () use (&$captured) {
        return new class($captured) {
            public function __construct(public &$cap) {}
            public function info(string $m, array $c): void { $this->cap = ['level' => 'info', 'ctx' => $c]; }
            public function error(string $m, array $c): void { $this->cap = ['level' => 'error', 'ctx' => $c]; }
            public function warning(string $m, array $c): void { $this->cap = ['level' => 'warning', 'ctx' => $c]; }
        };
    });

    $dec = new TelemetryDecorator(r9Network(), r9MakePlatform());
    try { $dec->pauseCampaign('t', 'rc', 'k'); } catch (NetworkException) {}

    expect($captured)->not->toBeNull()
        ->and($captured['level'])->toBe('error')
        ->and($captured['ctx']['status'])->toBe('error')
        ->and($captured['ctx']['error_class'])->toBe(NetworkException::class)
        ->and($captured['ctx']['error_code'])->toBe('TEST_NETWORK')
        ->and($captured['ctx']['error_message'])->toBe('downstream exploded');
});

test('non-AdPlatformException (RuntimeException) also logs at error level', function () {
    $captured = null;
    Log::partialMock()->shouldReceive('channel')->andReturnUsing(function () use (&$captured) {
        return new class($captured) {
            public function __construct(public &$cap) {}
            public function info(string $m, array $c): void { $this->cap = ['level' => 'info', 'ctx' => $c]; }
            public function error(string $m, array $c): void { $this->cap = ['level' => 'error', 'ctx' => $c]; }
            public function warning(string $m, array $c): void { $this->cap = ['level' => 'warning', 'ctx' => $c]; }
        };
    });

    $dec = new TelemetryDecorator(r9Generic(), r9MakePlatform());
    try { $dec->pauseCampaign('t', 'rc', 'k'); } catch (\RuntimeException) {}

    expect($captured)->not->toBeNull()
        ->and($captured['level'])->toBe('error')
        ->and($captured['ctx']['error_class'])->toBe('RuntimeException')
        ->and($captured['ctx']['error_message'])->toBe('boom');
});

// ============================================================
// trace_id uniqueness
// ============================================================

test('each call generates a distinct trace_id', function () {
    $traceIds = [];
    Log::partialMock()->shouldReceive('channel')->andReturnUsing(function () use (&$traceIds) {
        return new class($traceIds) {
            public function __construct(public &$t) {}
            public function info(string $m, array $c): void { $this->t[] = $c['trace_id']; }
            public function error(string $m, array $c): void { $this->t[] = $c['trace_id']; }
            public function warning(string $m, array $c): void { $this->t[] = $c['trace_id']; }
        };
    });

    $dec = new TelemetryDecorator(r9Successful(), r9MakePlatform());
    for ($i = 0; $i < 5; $i++) {
        $dec->pauseCampaign('t', 'rc', "k{$i}");
    }
    expect($traceIds)->toHaveCount(5)
        ->and(array_unique($traceIds))->toHaveCount(5); // all distinct
});

// ============================================================
// Method override coverage — every network-hitting method wrapped
// ============================================================

test('every network-hitting adapter method has a traced override', function () {
    $methods = [
        'handleCallback', 'refreshToken', 'getAdAccounts', 'healthCheck',
        'testAppCredentials', 'publishCampaign', 'pauseCampaign',
        'resumeCampaign', 'pullMetrics', 'syncAudience', 'deleteAudience',
        'getLeadForms', 'fetchLeads', 'createCatalog', 'syncCatalog',
        'getAuthUrl',
    ];

    $ref = new ReflectionClass(TelemetryDecorator::class);
    foreach ($methods as $m) {
        expect($ref->hasMethod($m))->toBeTrue("TelemetryDecorator must override {$m}");
        $method = $ref->getMethod($m);
        expect($method->getDeclaringClass()->getName())->toBe(TelemetryDecorator::class,
            "TelemetryDecorator::{$m} must be declared on the class (not inherited)");
    }
});

test('observability config defaults', function () {
    $default = require base_path('config/ads.php');
    expect($default['observability']['sample_rate_success'])->toBe(0.1)
        ->and($default['observability']['sample_rate_error'])->toBe(1.0);
});