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);
});