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

/**
 * Phase N — Admin Ads Settings + AdCredentialResolver + adapter interface tests.
 *
 * Tests the core credential resolution logic + DTOs + adapter interface contract.
 * Controller HTTP tests require full DB + auth stack — covered by E2E test instead.
 */

uses(Tests\TestCase::class);

use App\Services\Ads\AdCredentialResolver;
use App\Services\Ads\MockAdPlatform;
use App\Services\Ads\MetaAdPlatform;
use App\Services\Ads\GoogleAdPlatform;
use App\Services\Ads\TikTokAdPlatform;
use App\Services\Ads\LinkedInAdPlatform;
use App\Services\Ads\TestConnectionResult;
use App\Services\Ads\AdPlatformAdapter;

// ============================================================
// TestConnectionResult DTO
// ============================================================

test('test connection result has status constants', function () {
    expect(TestConnectionResult::STATUS_SUCCESS)->toBe('success');
    expect(TestConnectionResult::STATUS_ERROR)->toBe('error');
    expect(TestConnectionResult::STATUS_WARNING)->toBe('warning');
});

test('test connection result has review status constants', function () {
    expect(TestConnectionResult::REVIEW_LIVE)->toBe('live');
    expect(TestConnectionResult::REVIEW_DEVELOPMENT)->toBe('development');
    expect(TestConnectionResult::REVIEW_UNKNOWN)->toBe('unknown');
});

test('test connection result isSuccess returns true for success and warning', function () {
    $success = new TestConnectionResult(status: 'success', message: 'ok');
    $warning = new TestConnectionResult(status: 'warning', message: 'dev mode');
    $error = new TestConnectionResult(status: 'error', message: 'bad');

    expect($success->isSuccess())->toBeTrue();
    expect($warning->isSuccess())->toBeTrue();
    expect($error->isSuccess())->toBeFalse();
});

test('test connection result isDevMode returns true only for development status', function () {
    $dev = new TestConnectionResult(status: 'warning', message: '', appReviewStatus: 'development');
    $live = new TestConnectionResult(status: 'success', message: '', appReviewStatus: 'live');
    $unknown = new TestConnectionResult(status: 'success', message: '', appReviewStatus: null);

    expect($dev->isDevMode())->toBeTrue();
    expect($live->isDevMode())->toBeFalse();
    expect($unknown->isDevMode())->toBeFalse();
});

test('test connection result toArray includes all fields', function () {
    $result = new TestConnectionResult(
        status: 'warning',
        message: 'App in dev mode',
        appReviewStatus: 'development',
        resolutionHint: 'Submit for review',
    );

    $arr = $result->toArray();
    expect($arr)->toHaveKeys(['status', 'message', 'app_review_status', 'resolution_hint', 'is_dev_mode']);
    expect($arr['is_dev_mode'])->toBeTrue();
    expect($arr['app_review_status'])->toBe('development');
});

// ============================================================
// AdCredentialResolver
// ============================================================

test('resolver required keys defines all 4 platforms', function () {
    expect(AdCredentialResolver::REQUIRED_KEYS)->toHaveKeys(['meta', 'google', 'tiktok', 'linkedin']);
    expect(AdCredentialResolver::REQUIRED_KEYS['meta'])->toContain('app_id', 'app_secret');
    expect(AdCredentialResolver::REQUIRED_KEYS['google'])->toContain('client_id', 'client_secret', 'developer_token');
    expect(AdCredentialResolver::REQUIRED_KEYS['tiktok'])->toContain('app_id', 'app_secret');
    expect(AdCredentialResolver::REQUIRED_KEYS['linkedin'])->toContain('client_id', 'client_secret');
});

test('resolver required keys method returns array for each platform', function () {
    expect(AdCredentialResolver::requiredKeys('meta'))->toBeArray()->toHaveCount(2);
    expect(AdCredentialResolver::requiredKeys('google'))->toBeArray()->toHaveCount(3);
    expect(AdCredentialResolver::requiredKeys('tiktok'))->toBeArray()->toHaveCount(2);
    expect(AdCredentialResolver::requiredKeys('linkedin'))->toBeArray()->toHaveCount(2);
});

test('resolver required keys returns empty for unknown platform', function () {
    expect(AdCredentialResolver::requiredKeys('snapchat'))->toBe([]);
});

test('resolver resolves from config when present', function () {
    config(['ads.platforms.meta.app_id' => 'env_value_123']);
    expect(AdCredentialResolver::resolve('meta', 'app_id'))->toBe('env_value_123');
});

test('resolver falls back to setting when config empty', function () {
    config(['ads.platforms.meta.app_id' => null]);
    // setting() fallback — can't easily assert without DB, but verify no exception
    expect(fn() => AdCredentialResolver::resolve('meta', 'app_id'))->not->toThrow(Throwable::class);
});

test('resolver returns null when neither config nor setting has value', function () {
    config(['ads.platforms.tiktok.app_id' => null]);
    // Assuming test DB has no setting for this key
    $value = AdCredentialResolver::resolve('tiktok', 'nonexistent_key');
    expect($value)->toBeNull();
});

test('resolver isConfigured returns false when any required key is missing', function () {
    // Snapshot + clear settings for meta so the fallback chain (config → Setting)
    // doesn't pick up demo credentials seeded by Stage22_AdsSeeder. Restore after.
    $backup = \App\Model\Setting::where('name', 'like', 'ads.meta.%')
        ->get(['name', 'value'])->toArray();
    \App\Model\Setting::where('name', 'like', 'ads.meta.%')->delete();

    try {
        config([
            'ads.platforms.meta.app_id' => 'id_only',
            'ads.platforms.meta.app_secret' => null,
        ]);
        expect(AdCredentialResolver::isConfigured('meta'))->toBeFalse();
    } finally {
        // Restore the snapshot
        foreach ($backup as $row) {
            \App\Model\Setting::forceCreate(['name' => $row['name'], 'value' => $row['value']]);
        }
    }
});

test('resolver isConfigured returns true when all required keys present', function () {
    config([
        'ads.platforms.meta.app_id' => 'id_123',
        'ads.platforms.meta.app_secret' => 'secret_456',
    ]);
    expect(AdCredentialResolver::isConfigured('meta'))->toBeTrue();
});

test('resolver source returns env when config has value', function () {
    config(['ads.platforms.google.client_id' => 'env_id']);
    expect(AdCredentialResolver::source('google'))->toBe('env');
});

test('resolver all returns associative array of all required keys', function () {
    config([
        'ads.platforms.meta.app_id' => 'aaa',
        'ads.platforms.meta.app_secret' => 'bbb',
    ]);
    $all = AdCredentialResolver::all('meta');
    expect($all)->toHaveKeys(['app_id', 'app_secret']);
    expect($all['app_id'])->toBe('aaa');
    expect($all['app_secret'])->toBe('bbb');
});

// ============================================================
// AdPlatformAdapter interface — all adapters implement new methods
// ============================================================

test('all adapters implement getDefaultScopes', function () {
    $adapters = [
        new MockAdPlatform(),
        new MetaAdPlatform(),
        new GoogleAdPlatform(),
        new TikTokAdPlatform(),
        new LinkedInAdPlatform(),
    ];

    foreach ($adapters as $adapter) {
        expect($adapter)->toBeInstanceOf(AdPlatformAdapter::class);
        $scopes = $adapter->getDefaultScopes();
        expect($scopes)->toBeArray();
        expect(count($scopes))->toBeGreaterThanOrEqual(1);
    }
});

test('all adapters implement testAppCredentials', function () {
    $adapters = [
        new MockAdPlatform(),
    ];

    foreach ($adapters as $adapter) {
        $result = $adapter->testAppCredentials(['app_id' => 'test', 'app_secret' => 'test']);
        expect($result)->toBeInstanceOf(TestConnectionResult::class);
    }
});

test('meta adapter default scopes include ads_management', function () {
    $adapter = new MetaAdPlatform();
    expect($adapter->getDefaultScopes())->toContain('ads_management');
});

test('google adapter default scopes include adwords', function () {
    $adapter = new GoogleAdPlatform();
    $scopes = $adapter->getDefaultScopes();
    expect(implode(' ', $scopes))->toContain('adwords');
});

test('linkedin adapter default scopes include rw_ads', function () {
    $adapter = new LinkedInAdPlatform();
    expect($adapter->getDefaultScopes())->toContain('rw_ads');
});

test('tiktok adapter default scopes include advertiser.read', function () {
    $adapter = new TikTokAdPlatform();
    expect($adapter->getDefaultScopes())->toContain('advertiser.read');
});

// ============================================================
// Mock adapter — happy path + simulated failures
// ============================================================

test('mock adapter testAppCredentials returns success for valid creds', function () {
    $mock = new MockAdPlatform();
    $result = $mock->testAppCredentials(['app_id' => 'valid_id', 'app_secret' => 'valid_secret']);

    expect($result->status)->toBe(TestConnectionResult::STATUS_SUCCESS);
    expect($result->isSuccess())->toBeTrue();
});

test('mock adapter testAppCredentials returns error for MOCK_REJECT prefix', function () {
    $mock = new MockAdPlatform();
    $result = $mock->testAppCredentials(['app_id' => 'MOCK_REJECT_bad_id']);

    expect($result->status)->toBe(TestConnectionResult::STATUS_ERROR);
    expect($result->isSuccess())->toBeFalse();
});

test('mock adapter testAppCredentials returns warning + dev mode for MOCK_DEVMODE prefix', function () {
    $mock = new MockAdPlatform();
    $result = $mock->testAppCredentials(['app_id' => 'MOCK_DEVMODE_app']);

    expect($result->status)->toBe(TestConnectionResult::STATUS_WARNING);
    expect($result->isDevMode())->toBeTrue();
    expect($result->appReviewStatus)->toBe(TestConnectionResult::REVIEW_DEVELOPMENT);
});

test('mock adapter testAppCredentials returns error for empty creds', function () {
    $mock = new MockAdPlatform();
    $result = $mock->testAppCredentials([]);

    expect($result->status)->toBe(TestConnectionResult::STATUS_ERROR);
});