620 lines
22 KiB
PHP
620 lines
22 KiB
PHP
<?php
|
||
|
||
use App\Models\Player;
|
||
use App\Models\AuditLog;
|
||
use App\Models\AdminSite;
|
||
use App\Models\AdminUser;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Database\Seeders\CurrencySeeder;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Illuminate\Support\Facades\Http;
|
||
use App\Services\Integration\PartnerSiteConfig;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||
|
||
uses(RefreshDatabase::class);
|
||
|
||
beforeEach(function (): void {
|
||
fakeWalletApiDns();
|
||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||
});
|
||
|
||
function integrationAdminToken(): string
|
||
{
|
||
$admin = AdminUser::query()->create([
|
||
'username' => 'integration_admin',
|
||
'name' => 'Integration Admin',
|
||
'email' => null,
|
||
'password' => Hash::make('secret-strong'),
|
||
'status' => 0,
|
||
]);
|
||
grantSuperAdminRole($admin);
|
||
|
||
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||
}
|
||
|
||
test('super admin can create integration site and receive secrets once', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'partner-a',
|
||
'name' => 'Partner A',
|
||
'wallet_api_url' => 'https://wallet.partner-a.test',
|
||
'status' => 1,
|
||
'admin_account' => [
|
||
'username' => 'partner_a_admin',
|
||
'nickname' => 'Partner A Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
|
||
$response->assertCreated()
|
||
->assertJsonPath('code', 0)
|
||
->assertJsonPath('data.code', 'partner-a')
|
||
->assertJsonPath('data.secrets_display_once', true)
|
||
->assertJsonPath('data.admin_user.username', 'partner_a_admin')
|
||
->assertJsonStructure([
|
||
'data' => [
|
||
'secrets' => ['sso_jwt_secret', 'wallet_api_key'],
|
||
'admin_user' => ['id', 'username', 'nickname', 'email'],
|
||
],
|
||
]);
|
||
|
||
$site = AdminSite::query()->where('code', 'partner-a')->first();
|
||
expect($site)->not->toBeNull();
|
||
expect($site?->decryptedSsoJwtSecret())->not->toBeEmpty();
|
||
|
||
expect(AuditLog::query()->where('module_code', 'integration')->where('action_code', 'create')->exists())->toBeTrue();
|
||
});
|
||
|
||
test('super admin can reveal integration site secrets for copy', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'partner-secrets',
|
||
'name' => 'Partner Secrets',
|
||
'admin_account' => [
|
||
'username' => 'partner_secrets_admin',
|
||
'nickname' => 'Partner Secrets Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertCreated();
|
||
|
||
$id = (int) $create->json('data.id');
|
||
$plainSso = (string) $create->json('data.secrets.sso_jwt_secret');
|
||
$plainWallet = (string) $create->json('data.secrets.wallet_api_key');
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->getJson('/api/v1/admin/integration-sites/'.$id.'/secrets')
|
||
->assertOk()
|
||
->assertJsonPath('data.sso_jwt_secret', $plainSso)
|
||
->assertJsonPath('data.wallet_api_key', $plainWallet);
|
||
|
||
expect(
|
||
AuditLog::query()
|
||
->where('module_code', 'integration')
|
||
->where('action_code', 'reveal_secrets')
|
||
->exists()
|
||
)->toBeTrue();
|
||
});
|
||
|
||
test('integration site code cannot be changed on update', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'partner-b',
|
||
'name' => 'Partner B',
|
||
'admin_account' => [
|
||
'username' => 'partner_b_admin',
|
||
'nickname' => 'Partner B Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
$create->assertCreated();
|
||
$id = (int) $create->json('data.id');
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->putJson('/api/v1/admin/integration-sites/'.$id, [
|
||
'name' => 'Partner B Renamed',
|
||
'status' => 1,
|
||
])
|
||
->assertOk()
|
||
->assertJsonPath('data.code', 'partner-b')
|
||
->assertJsonPath('data.name', 'Partner B Renamed');
|
||
});
|
||
|
||
test('partner site config resolver cache roundtrip returns partner site config', function (): void {
|
||
AdminSite::query()->create([
|
||
'code' => 'cache-roundtrip',
|
||
'name' => 'Cache',
|
||
'currency_code' => 'NPR',
|
||
'status' => 1,
|
||
'is_default' => false,
|
||
'sso_jwt_secret_encrypted' => encrypt('cache-sso'),
|
||
'wallet_api_key_encrypted' => encrypt('cache-wallet'),
|
||
]);
|
||
|
||
$resolver = app(PartnerSiteConfigResolver::class);
|
||
$first = $resolver->resolveBySiteCode('cache-roundtrip');
|
||
$second = $resolver->resolveBySiteCode('cache-roundtrip');
|
||
|
||
expect($second)->toBeInstanceOf(PartnerSiteConfig::class)
|
||
->and($second->ssoJwtSecret)->toBe('cache-sso');
|
||
});
|
||
|
||
test('partner site config resolver reads database secrets', function (): void {
|
||
AdminSite::query()->create([
|
||
'code' => 'partner-db',
|
||
'name' => 'DB Partner',
|
||
'currency_code' => 'NPR',
|
||
'status' => 1,
|
||
'is_default' => false,
|
||
'sso_jwt_secret_encrypted' => encrypt('db-sso-secret'),
|
||
'wallet_api_key_encrypted' => encrypt('db-wallet-key'),
|
||
'wallet_api_url' => 'https://wallet.db.test',
|
||
]);
|
||
|
||
$config = app(PartnerSiteConfigResolver::class)->resolveBySiteCode('partner-db');
|
||
|
||
expect($config->source)->toBe('database')
|
||
->and($config->ssoJwtSecret)->toBe('db-sso-secret')
|
||
->and($config->walletApiKey)->toBe('db-wallet-key')
|
||
->and($config->walletApiUrl)->toBe('https://wallet.db.test');
|
||
});
|
||
|
||
test('rotate secrets returns new plaintext once', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'partner-rotate',
|
||
'name' => 'Rotate',
|
||
'admin_account' => [
|
||
'username' => 'partner_rotate_admin',
|
||
'nickname' => 'Partner Rotate Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
$id = (int) $create->json('data.id');
|
||
$oldSecret = (string) $create->json('data.secrets.sso_jwt_secret');
|
||
|
||
$rotate = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites/'.$id.'/rotate-secrets');
|
||
$rotate->assertOk();
|
||
$newSecret = (string) $rotate->json('data.secrets.sso_jwt_secret');
|
||
|
||
expect($newSecret)->not->toBe($oldSecret);
|
||
|
||
expect(AuditLog::query()->where('action_code', 'rotate_secrets')->exists())->toBeTrue();
|
||
});
|
||
|
||
test('connectivity test probes partner balance api', function (): void {
|
||
Http::fake([
|
||
'https://wallet.probe.test/*' => Http::response([
|
||
'success' => true,
|
||
'data' => ['main_balance' => 12345, 'currency_code' => 'NPR'],
|
||
], 200),
|
||
]);
|
||
|
||
$token = integrationAdminToken();
|
||
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'probe-site',
|
||
'name' => 'Probe',
|
||
'wallet_api_url' => 'https://wallet.probe.test',
|
||
'admin_account' => [
|
||
'username' => 'probe_site_admin',
|
||
'nickname' => 'Probe Site Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
$id = (int) $create->json('data.id');
|
||
|
||
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites/'.$id.'/connectivity-test', [
|
||
'site_player_id' => '10001',
|
||
'currency_code' => 'NPR',
|
||
]);
|
||
|
||
$response->assertOk()
|
||
->assertJsonPath('data.probe.success', true)
|
||
->assertJsonPath('data.probe.main_balance_minor', 12345)
|
||
->assertJsonMissingPath('data.probe.response_preview');
|
||
});
|
||
|
||
test('connectivity test rejects hostname resolving to a private address before sending', function (): void {
|
||
fakeWalletApiDns([
|
||
'wallet.private.test' => ['93.184.216.34', '10.0.0.8'],
|
||
], []);
|
||
Http::preventStrayRequests();
|
||
|
||
$token = integrationAdminToken();
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'private-probe-site',
|
||
'name' => 'Private Probe',
|
||
'wallet_api_url' => 'https://wallet.private.test',
|
||
'admin_account' => [
|
||
'username' => 'private_probe_admin',
|
||
'nickname' => 'Private Probe Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites/'.(int) $create->json('data.id').'/connectivity-test', [
|
||
'site_player_id' => '10001',
|
||
'currency_code' => 'NPR',
|
||
])
|
||
->assertOk()
|
||
->assertJsonPath('data.probe.success', false)
|
||
->assertJsonPath('data.probe.http_status', null)
|
||
->assertJsonPath('data.probe.message', 'wallet_api_url 无效(拒绝以防 SSRF)')
|
||
->assertJsonMissingPath('data.probe.response_preview');
|
||
|
||
Http::assertSentCount(0);
|
||
});
|
||
|
||
test('connectivity test does not follow redirects or expose response preview', function (): void {
|
||
fakeWalletApiDns([
|
||
'wallet.redirect.test' => ['93.184.216.34'],
|
||
], []);
|
||
Http::fake([
|
||
'https://wallet.redirect.test/*' => Http::response(
|
||
['secret' => 'must-not-be-returned'],
|
||
302,
|
||
['Location' => 'https://169.254.169.254/latest/meta-data'],
|
||
),
|
||
'https://169.254.169.254/*' => Http::response(['role' => 'internal'], 200),
|
||
]);
|
||
|
||
$token = integrationAdminToken();
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'redirect-probe-site',
|
||
'name' => 'Redirect Probe',
|
||
'wallet_api_url' => 'https://wallet.redirect.test',
|
||
'admin_account' => [
|
||
'username' => 'redirect_probe_admin',
|
||
'nickname' => 'Redirect Probe Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites/'.(int) $create->json('data.id').'/connectivity-test', [
|
||
'site_player_id' => '10001',
|
||
'currency_code' => 'NPR',
|
||
])
|
||
->assertOk()
|
||
->assertJsonPath('data.probe.success', false)
|
||
->assertJsonPath('data.probe.http_status', 302)
|
||
->assertJsonMissingPath('data.probe.response_preview')
|
||
->assertJsonMissing(['must-not-be-returned']);
|
||
|
||
Http::assertSentCount(1);
|
||
});
|
||
|
||
test('export parameter sheet excludes plaintext secrets', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'export-site',
|
||
'name' => 'Export',
|
||
'wallet_api_url' => 'https://wallet.export.test',
|
||
'admin_account' => [
|
||
'username' => 'export_site_admin',
|
||
'nickname' => 'Export Site Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
]);
|
||
$id = (int) $create->json('data.id');
|
||
|
||
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->getJson('/api/v1/admin/integration-sites/'.$id.'/export');
|
||
|
||
$response->assertOk()
|
||
->assertJsonPath('data.site_code', 'export-site')
|
||
->assertJsonPath('data.sso_secret_masked', '••••••••')
|
||
->assertJsonMissingPath('data.secrets')
|
||
->assertJsonMissingPath('data.sso_jwt_secret');
|
||
});
|
||
|
||
test('site scoped admin only sees bound integration sites', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'site-a',
|
||
'name' => 'Site A',
|
||
'admin_account' => [
|
||
'username' => 'site_a_admin',
|
||
'nickname' => 'Site A Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertCreated();
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'site-b',
|
||
'name' => 'Site B',
|
||
'admin_account' => [
|
||
'username' => 'site_b_admin',
|
||
'nickname' => 'Site B Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertCreated();
|
||
|
||
$siteAId = (int) AdminSite::query()->where('code', 'site-a')->value('id');
|
||
$siteBId = (int) AdminSite::query()->where('code', 'site-b')->value('id');
|
||
|
||
$scopedAdmin = AdminUser::query()->create([
|
||
'username' => 'integration_scoped',
|
||
'name' => 'Scoped',
|
||
'email' => null,
|
||
'password' => Hash::make('secret-strong'),
|
||
'status' => 0,
|
||
]);
|
||
|
||
$roleId = (int) DB::table('admin_roles')->insertGetId([
|
||
'slug' => 'integration_scoped_role',
|
||
'name' => 'Integration Scoped',
|
||
'code' => 'integration_scoped_role',
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$viewActionId = (int) DB::table('admin_menu_actions')
|
||
->where('permission_code', 'integration.site.view')
|
||
->value('id');
|
||
|
||
DB::table('admin_role_menu_actions')->insert([
|
||
'role_id' => $roleId,
|
||
'menu_action_id' => $viewActionId,
|
||
]);
|
||
|
||
DB::table('admin_user_site_roles')->insert([
|
||
'admin_user_id' => $scopedAdmin->id,
|
||
'site_id' => $siteAId,
|
||
'role_id' => $roleId,
|
||
'granted_at' => now(),
|
||
]);
|
||
|
||
$scopedToken = $scopedAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||
|
||
app('auth')->forgetGuards();
|
||
|
||
$list = $this->withHeader('Authorization', 'Bearer '.$scopedToken)
|
||
->getJson('/api/v1/admin/integration-sites')
|
||
->assertOk();
|
||
|
||
$codes = collect($list->json('data.items'))->pluck('code')->all();
|
||
expect($codes)->toContain('site-a')->not->toContain('site-b');
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$scopedToken)
|
||
->getJson('/api/v1/admin/integration-sites/'.$siteBId)
|
||
->assertForbidden();
|
||
});
|
||
|
||
test('player list is filtered by admin site binding', function (): void {
|
||
$this->seed(CurrencySeeder::class);
|
||
|
||
Player::query()->create([
|
||
'site_code' => 'site-a',
|
||
'site_player_id' => 'pa-1',
|
||
'username' => 'pa1',
|
||
'nickname' => 'PA1',
|
||
'default_currency' => 'NPR',
|
||
'status' => 0,
|
||
]);
|
||
Player::query()->create([
|
||
'site_code' => 'site-b',
|
||
'site_player_id' => 'pb-1',
|
||
'username' => 'pb1',
|
||
'nickname' => 'PB1',
|
||
'default_currency' => 'NPR',
|
||
'status' => 0,
|
||
]);
|
||
|
||
AdminSite::query()->firstOrCreate(['code' => 'site-a'], ['name' => 'A', 'currency_code' => 'NPR', 'status' => 1]);
|
||
AdminSite::query()->firstOrCreate(['code' => 'site-b'], ['name' => 'B', 'currency_code' => 'NPR', 'status' => 1]);
|
||
$siteAId = (int) AdminSite::query()->where('code', 'site-a')->value('id');
|
||
|
||
$scopedAdmin = AdminUser::query()->create([
|
||
'username' => 'player_scoped',
|
||
'name' => 'Player Scoped',
|
||
'email' => null,
|
||
'password' => Hash::make('secret-strong'),
|
||
'status' => 0,
|
||
]);
|
||
|
||
$roleId = (int) DB::table('admin_roles')->insertGetId([
|
||
'slug' => 'player_scoped_role',
|
||
'name' => 'Player Scoped',
|
||
'code' => 'player_scoped_role',
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$viewActionId = (int) DB::table('admin_menu_actions')
|
||
->where('permission_code', 'service.players.view')
|
||
->value('id');
|
||
|
||
DB::table('admin_role_menu_actions')->insert([
|
||
'role_id' => $roleId,
|
||
'menu_action_id' => $viewActionId,
|
||
]);
|
||
|
||
DB::table('admin_user_site_roles')->insert([
|
||
'admin_user_id' => $scopedAdmin->id,
|
||
'site_id' => $siteAId,
|
||
'role_id' => $roleId,
|
||
'granted_at' => now(),
|
||
]);
|
||
|
||
$scopedToken = $scopedAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||
|
||
$response = $this->withHeader('Authorization', 'Bearer '.$scopedToken)
|
||
->getJson('/api/v1/admin/players')
|
||
->assertOk();
|
||
|
||
$siteCodes = collect($response->json('data.items'))->pluck('site_code')->unique()->values()->all();
|
||
expect($siteCodes)->toBe(['site-a']);
|
||
});
|
||
|
||
test('wallet_api_url rejects non-https', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'bad-https-1',
|
||
'name' => 'Bad HTTPS 1',
|
||
'wallet_api_url' => 'http://wallet.bad.test',
|
||
'admin_account' => [
|
||
'username' => 'bad_https_admin_1',
|
||
'nickname' => 'Bad HTTPS Admin 1',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertStatus(422)
|
||
->assertJsonPath('data.errors.wallet_api_url.0', fn (string $msg): bool => str_contains($msg, 'https'));
|
||
});
|
||
|
||
test('wallet_api_url rejects localhost', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'bad-https-2',
|
||
'name' => 'Bad HTTPS 2',
|
||
'wallet_api_url' => 'https://localhost:8080',
|
||
'admin_account' => [
|
||
'username' => 'bad_https_admin_2',
|
||
'nickname' => 'Bad HTTPS Admin 2',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertStatus(422)
|
||
->assertJsonPath('data.errors.wallet_api_url.0', fn (string $msg): bool => str_contains($msg, 'localhost') || str_contains($msg, 'private'));
|
||
});
|
||
|
||
test('wallet_api_url rejects private ip with path', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'bad-https-3',
|
||
'name' => 'Bad HTTPS 3',
|
||
'wallet_api_url' => 'https://127.0.0.1/wallet',
|
||
'admin_account' => [
|
||
'username' => 'bad_https_admin_3',
|
||
'nickname' => 'Bad HTTPS Admin 3',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertStatus(422)
|
||
->assertJsonPath('data.errors.wallet_api_url.0', fn (string $msg): bool => str_contains($msg, 'https'));
|
||
});
|
||
|
||
test('super admin can delete integration site and cleanup related data', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$create = $this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'partner-del',
|
||
'name' => 'Partner Delete Me',
|
||
'admin_account' => [
|
||
'username' => 'partner_del_admin',
|
||
'nickname' => 'Partner Del Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertCreated();
|
||
|
||
$id = (int) $create->json('data.id');
|
||
|
||
Player::query()->create([
|
||
'site_code' => 'partner-del',
|
||
'site_player_id' => '90001',
|
||
'username' => 'partner_del_player',
|
||
'nickname' => 'Partner Del Player',
|
||
'default_currency' => 'NPR',
|
||
'status' => 1,
|
||
]);
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->deleteJson('/api/v1/admin/integration-sites/'.$id)
|
||
->assertOk()
|
||
->assertJsonPath('code', 0);
|
||
|
||
expect(AdminSite::query()->where('code', 'partner-del')->exists())->toBeFalse();
|
||
expect(Player::query()->where('site_code', 'partner-del')->exists())->toBeFalse();
|
||
expect(AdminUser::query()->where('username', 'partner_del_admin')->exists())->toBeFalse();
|
||
expect(DB::table('admin_roles')->where('slug', 'site_admin')->exists())->toBeTrue();
|
||
expect(DB::table('admin_roles')->where('slug', 'site_admin_partner-del')->exists())->toBeFalse();
|
||
|
||
expect(
|
||
AuditLog::query()
|
||
->where('module_code', 'integration')
|
||
->where('action_code', 'destroy')
|
||
->where('target_id', (string) $id)
|
||
->exists()
|
||
)->toBeTrue();
|
||
});
|
||
|
||
test('super admin can delete default integration site when another site exists', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->postJson('/api/v1/admin/integration-sites', [
|
||
'code' => 'partner-keep',
|
||
'name' => 'Partner Keep',
|
||
'admin_account' => [
|
||
'username' => 'partner_keep_admin',
|
||
'nickname' => 'Partner Keep Admin',
|
||
'password' => 'secret-strong',
|
||
],
|
||
])
|
||
->assertCreated();
|
||
|
||
$defaultSite = AdminSite::query()->where('is_default', true)->firstOrFail();
|
||
$defaultSiteId = (int) $defaultSite->id;
|
||
$superAdminId = (int) AdminUser::query()->where('username', 'integration_admin')->value('id');
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->deleteJson('/api/v1/admin/integration-sites/'.$defaultSiteId)
|
||
->assertOk()
|
||
->assertJsonPath('code', 0);
|
||
|
||
expect(AdminSite::query()->where('id', $defaultSiteId)->exists())->toBeFalse();
|
||
expect(AdminUser::query()->where('id', $superAdminId)->exists())->toBeTrue();
|
||
expect(AdminUser::query()->where('id', $superAdminId)->value('is_super_admin'))->toBeTruthy();
|
||
});
|
||
|
||
test('super admin can delete last integration site and remain authenticated', function (): void {
|
||
$token = integrationAdminToken();
|
||
|
||
foreach (AdminSite::query()->orderBy('id')->pluck('id') as $siteId) {
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->deleteJson('/api/v1/admin/integration-sites/'.$siteId)
|
||
->assertOk()
|
||
->assertJsonPath('code', 0);
|
||
}
|
||
|
||
expect(AdminSite::query()->count())->toBe(0);
|
||
|
||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||
->getJson('/api/v1/admin/auth/me')
|
||
->assertOk()
|
||
->assertJsonPath('data.admin.is_super_admin', true)
|
||
->assertJsonPath('data.admin.accessible_sites', []);
|
||
});
|