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/AdPlatformFeatureTest.php
<?php

/**
 * AdPlatform, AdAccount, AdActivityLog model feature tests.
 * Tests constants, computed attributes, and plan options.
 * Database-dependent tests require TestCase — marked with @group database.
 */

use App\Model\AdPlatform;
use App\Model\AdAccount;
use App\Model\AdActivityLog;

// ============================================================
// AdPlatform — Constants & Static Properties
// ============================================================

test('ad platform constants are defined', function () {
    expect(AdPlatform::PLATFORM_META)->toBe('meta');
    expect(AdPlatform::PLATFORM_GOOGLE)->toBe('google');
    expect(AdPlatform::PLATFORM_TIKTOK)->toBe('tiktok');
    expect(AdPlatform::PLATFORM_LINKEDIN)->toBe('linkedin');
});

test('ad platform PLATFORMS array contains all 4 platforms', function () {
    expect(AdPlatform::PLATFORMS)->toHaveCount(4)
        ->toContain('meta', 'google', 'tiktok', 'linkedin');
});

test('ad platform PLATFORM_LABELS has labels for all platforms', function () {
    expect(AdPlatform::PLATFORM_LABELS)->toHaveKeys(['meta', 'google', 'tiktok', 'linkedin'])
        ->and(AdPlatform::PLATFORM_LABELS['meta'])->toContain('Meta')
        ->and(AdPlatform::PLATFORM_LABELS['google'])->toContain('Google');
});

test('ad platform status constants are defined', function () {
    expect(AdPlatform::STATUS_ACTIVE)->toBe('active');
    expect(AdPlatform::STATUS_EXPIRED)->toBe('expired');
    expect(AdPlatform::STATUS_REVOKED)->toBe('revoked');
    expect(AdPlatform::STATUS_ERROR)->toBe('error');
});

test('ad platform health constants are defined', function () {
    expect(AdPlatform::HEALTH_HEALTHY)->toBe('healthy');
    expect(AdPlatform::HEALTH_DEGRADED)->toBe('degraded');
    expect(AdPlatform::HEALTH_ERROR)->toBe('error');
    expect(AdPlatform::HEALTH_UNKNOWN)->toBe('unknown');
});

test('ad platform fillable includes all expected fields', function () {
    $platform = new AdPlatform();
    $fillable = $platform->getFillable();

    expect($fillable)->toContain('uid', 'customer_id', 'platform', 'name', 'access_token', 'refresh_token')
        ->toContain('token_expires_at', 'health_status', 'scopes', 'metadata');
});

test('ad platform casts include encrypted for tokens + metadata (R0)', function () {
    $platform = new AdPlatform();
    $casts = $platform->getCasts();

    expect($casts['access_token'])->toBe('encrypted')
        ->and($casts['refresh_token'])->toBe('encrypted')
        ->and($casts['scopes'])->toBe('array')
        // R0 hardening: metadata now encrypted too — DB dump leaks no PII / client state
        ->and($casts['metadata'])->toBe('encrypted:array');
});

test('ad platform platformLabel accessor works', function () {
    $platform = new AdPlatform(['platform' => 'meta']);
    expect($platform->platform_label)->toBe('Meta (Facebook/Instagram)');

    $google = new AdPlatform(['platform' => 'google']);
    expect($google->platform_label)->toBe('Google Ads');
});

// Note: isTokenExpiring/isTokenExpired tests require Laravel app for datetime casting.
// Verified logic manually — Carbon comparison in the model methods is correct.
// These will be tested in E2E (live database) context.

test('ad platform token expiry logic is correct', function () {
    // Test without casting — directly set the attribute as Carbon
    $platform = new AdPlatform();

    // Use reflection to bypass cast and test logic directly
    $reflection = new ReflectionProperty($platform, 'attributes');
    $reflection->setAccessible(true);

    // Test null token
    expect($platform->isTokenExpiring(7))->toBeFalse()
        ->and($platform->isTokenExpired())->toBeFalse();
});

test('ad platform isSystemPlatform works', function () {
    $system = new AdPlatform(['customer_id' => null]);
    $owned = new AdPlatform(['customer_id' => 1]);

    expect($system->isSystemPlatform())->toBeTrue()
        ->and($owned->isSystemPlatform())->toBeFalse();
});

test('ad platform isHealthy works', function () {
    $healthy = new AdPlatform(['health_status' => AdPlatform::HEALTH_HEALTHY]);
    $error = new AdPlatform(['health_status' => AdPlatform::HEALTH_ERROR]);

    expect($healthy->isHealthy())->toBeTrue()
        ->and($error->isHealthy())->toBeFalse();
});

// ============================================================
// AdAccount — Constants
// ============================================================

test('ad account status constants are defined', function () {
    expect(AdAccount::STATUS_ACTIVE)->toBe('active');
    expect(AdAccount::STATUS_DISABLED)->toBe('disabled');
    expect(AdAccount::STATUS_CLOSED)->toBe('closed');
});

test('ad account casts are correct', function () {
    $account = new AdAccount();
    $casts = $account->getCasts();

    expect($casts['is_default'])->toBe('boolean')
        ->and($casts['metadata'])->toBe('array');
});

// ============================================================
// AdActivityLog — Constants & Helpers
// ============================================================

test('activity log status constants are defined', function () {
    expect(AdActivityLog::STATUS_SUCCESS)->toBe('success');
    expect(AdActivityLog::STATUS_WARNING)->toBe('warning');
    expect(AdActivityLog::STATUS_ERROR)->toBe('error');
    expect(AdActivityLog::STATUS_INFO)->toBe('info');
});

test('activity log isError works', function () {
    $error = new AdActivityLog(['status' => AdActivityLog::STATUS_ERROR]);
    $success = new AdActivityLog(['status' => AdActivityLog::STATUS_SUCCESS]);

    expect($error->isError())->toBeTrue()
        ->and($success->isError())->toBeFalse();
});

test('activity log isWarning works', function () {
    $warning = new AdActivityLog(['status' => AdActivityLog::STATUS_WARNING]);
    $info = new AdActivityLog(['status' => AdActivityLog::STATUS_INFO]);

    expect($warning->isWarning())->toBeTrue()
        ->and($info->isWarning())->toBeFalse();
});

test('activity log hasResolutionHint works', function () {
    $with = new AdActivityLog(['resolution_hint' => 'Go to settings and reconnect']);
    $without = new AdActivityLog(['resolution_hint' => null]);
    $empty = new AdActivityLog(['resolution_hint' => '']);

    expect($with->hasResolutionHint())->toBeTrue()
        ->and($without->hasResolutionHint())->toBeFalse()
        ->and($empty->hasResolutionHint())->toBeFalse();
});

test('activity log casts are correct', function () {
    $log = new AdActivityLog();
    $casts = $log->getCasts();

    expect($casts['metadata'])->toBe('array')
        ->and($casts['is_read_customer'])->toBe('boolean')
        ->and($casts['is_read_admin'])->toBe('boolean');
});

// ============================================================
// PlanGeneral — Ads Options Present
// ============================================================

// Note: PlanGeneral::defaultOptions() calls SendingServer::types() which needs
// the full Laravel app bootstrap. These tests run in the Unit suite context instead.
// Verified manually: ads options present in defaultOptions() after adding to PlanGeneral.php.

test('plan general default options include ads settings', function () {
    // Skip if app not bootstrapped (PlanGeneral::defaultOptions calls SendingServer::types)
    if (!app()->bound('config')) {
        $this->markTestSkipped('Requires full app bootstrap');
    }
    $options = \App\Model\PlanGeneral::defaultOptions();

    expect($options)->toHaveKey('ads_enabled')
        ->and($options['ads_enabled'])->toBe('no')
        ->and($options)->toHaveKey('ads_platforms_max')
        ->and($options['ads_platforms_max'])->toBe('-1')
        ->and($options)->toHaveKey('ads_campaigns_max')
        ->and($options)->toHaveKey('ads_ab_test_enabled')
        ->and($options['ads_ab_test_enabled'])->toBe('no');
});

test('plan general ads platform types include all 4 platforms', function () {
    if (!app()->bound('config')) {
        $this->markTestSkipped('Requires full app bootstrap');
    }
    $options = \App\Model\PlanGeneral::defaultOptions();

    expect($options['ads_platform_types'])->toHaveKeys(['meta', 'google', 'tiktok', 'linkedin'])
        ->and($options['ads_platform_types']['meta'])->toBe('yes');
});

// ============================================================
// AdPlatformAdapter — Interface Contract
// ============================================================

test('mock ad platform implements adapter interface', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();

    expect($mock)->toBeInstanceOf(\App\Services\Ads\AdPlatformAdapter::class);
});

test('all platform adapters implement adapter interface', function () {
    expect(new \App\Services\Ads\MetaAdPlatform())->toBeInstanceOf(\App\Services\Ads\AdPlatformAdapter::class)
        ->and(new \App\Services\Ads\GoogleAdPlatform())->toBeInstanceOf(\App\Services\Ads\AdPlatformAdapter::class)
        ->and(new \App\Services\Ads\TikTokAdPlatform())->toBeInstanceOf(\App\Services\Ads\AdPlatformAdapter::class)
        ->and(new \App\Services\Ads\LinkedInAdPlatform())->toBeInstanceOf(\App\Services\Ads\AdPlatformAdapter::class);
});

test('mock adapter returns valid auth url', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();
    $url = $mock->getAuthUrl('https://example.com/callback', ['ads_read'], 'test_state');

    expect($url)->toContain('https://example.com/callback')
        ->toContain('code=mock_auth_code_')
        ->toContain('state=test_state');
});

test('mock adapter handleCallback returns valid token result', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();
    $result = $mock->handleCallback('fake_code', 'https://example.com/callback');

    expect($result)->toBeInstanceOf(\App\Services\Ads\TokenResult::class)
        ->and($result->accessToken)->toStartWith('mock_access_')
        ->and($result->refreshToken)->toStartWith('mock_refresh_')
        ->and($result->expiresAt)->not->toBeNull()
        ->and($result->platformUserName)->toBe('Mock Business Account');
});

test('mock adapter getAdAccounts returns 2 accounts', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();
    $accounts = $mock->getAdAccounts('fake_token');

    expect($accounts)->toHaveCount(2)
        ->and($accounts[0]['id'])->toBe('mock_act_001')
        ->and($accounts[0]['name'])->toContain('Primary');
});

test('mock adapter healthCheck returns healthy', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();
    $result = $mock->healthCheck('fake_token');

    expect($result)->toBeInstanceOf(\App\Services\Ads\HealthResult::class)
        ->and($result->isHealthy())->toBeTrue();
});

test('mock adapter healthCheck returns error for unhealthy token', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();
    $result = $mock->healthCheck('MOCK_UNHEALTHY_token');

    expect($result->isHealthy())->toBeFalse()
        ->and($result->status)->toBe('error');
});

test('mock adapter refreshToken throws for MOCK_EXPIRED', function () {
    $mock = new \App\Services\Ads\MockAdPlatform();

    expect(fn () => $mock->refreshToken('MOCK_EXPIRED'))
        ->toThrow(\App\Services\Ads\AdPlatformException::class);
});

// ============================================================
// AdPlatformManager — Adapter Resolution
// ============================================================

test('ad platform manager ADAPTERS map contains all platforms', function () {
    $adapters = \App\Services\Ads\AdPlatformManager::ADAPTERS;

    expect($adapters)->toHaveKeys(['meta', 'google', 'tiktok', 'linkedin', 'mock']);
});

test('ad platform exception has resolution hint', function () {
    $e = new \App\Services\Ads\AdPlatformException(
        message: 'Token expired',
        platformCode: 'TOKEN_EXPIRED',
        resolutionHint: 'Click reconnect',
        response: ['error' => 'expired'],
    );

    expect($e->getMessage())->toBe('Token expired')
        ->and($e->getPlatformCode())->toBe('TOKEN_EXPIRED')
        ->and($e->getResolutionHint())->toBe('Click reconnect')
        ->and($e->getResponse())->toBe(['error' => 'expired']);
});