Files
lotteryLaravel/tests/Feature/PlayerNativeJwtSecretGuardTest.php

175 lines
6.5 KiB
PHP

<?php
use Firebase\JWT\JWT;
use App\Models\Player;
use App\Lottery\ErrorCode;
use App\Support\PlayerAuthSource;
use App\Support\PlayerFundingMode;
use Illuminate\Support\Facades\DB;
use Database\Seeders\CurrencySeeder;
use Illuminate\Support\Facades\Hash;
use Database\Seeders\LotterySettingsSeeder;
use App\Services\Player\PlayerNativeAuthService;
use App\Exceptions\PlayerAuthenticationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function (): void {
config([
'lottery.player_auth.native.secret' => 'independent-native-secret-32bytes!!',
'lottery.player_auth.native.ttl_seconds' => 3600,
'lottery.main_site.sso_jwt_secret' => null,
'lottery.main_site.wallet_api_url' => null,
]);
$this->seed(CurrencySeeder::class);
$this->seed(LotterySettingsSeeder::class);
});
function nativeSecretGuardPlayer(string $suffix): Player
{
$site = DB::table('admin_sites')->where('is_default', true)->first();
$rootId = (int) DB::table('agent_nodes')->where('depth', 0)->value('id');
return Player::query()->create([
'site_code' => (string) $site->code,
'agent_node_id' => $rootId,
'site_player_id' => 'native:secret-guard-'.$suffix,
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
'funding_mode' => PlayerFundingMode::CREDIT,
'username' => 'secret_guard_'.$suffix,
'password_hash' => Hash::make('secret-pass'),
'default_currency' => 'NPR',
'status' => 0,
]);
}
function nativeSecretGuardToken(Player $player, string $secret): string
{
$now = time();
return JWT::encode([
'player_id' => (int) $player->id,
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
'token_version' => (int) ($player->native_token_version ?? 0),
'site_code' => (string) $player->site_code,
'iat' => $now,
'exp' => $now + 3600,
], $secret, 'HS256');
}
function expectNativeSecretConfigurationRejected(Closure $callback): void
{
try {
$callback();
test()->fail('Expected native JWT secret configuration to be rejected.');
} catch (PlayerAuthenticationException $exception) {
expect($exception->lotteryCode)->toBe(ErrorCode::PlayerSsoSecretNotConfigured->value)
->and($exception->httpStatus)->toBe(503);
}
}
test('missing native secret rejects token issuance and verification with 503', function (): void {
$player = nativeSecretGuardPlayer('missing');
config(['lottery.player_auth.native.secret' => null]);
expectNativeSecretConfigurationRejected(
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
);
$token = nativeSecretGuardToken($player, 'previous-native-secret-32bytes!!');
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/player/me')
->assertStatus(503)
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
});
test('native secret matching legacy sso secret rejects token issuance and verification', function (): void {
$player = nativeSecretGuardPlayer('legacy-match');
$sharedSecret = 'shared-legacy-native-secret-32bytes!!';
config([
'lottery.player_auth.native.secret' => $sharedSecret,
'lottery.main_site.sso_jwt_secret' => $sharedSecret,
]);
expectNativeSecretConfigurationRejected(
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
);
$token = nativeSecretGuardToken($player, $sharedSecret);
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/player/me')
->assertStatus(503)
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
});
test('native secret matching enabled database site sso secret rejects issuance and verification', function (): void {
$player = nativeSecretGuardPlayer('database-match');
$sharedSecret = 'shared-database-native-secret-32bytes!!';
config([
'lottery.player_auth.native.secret' => $sharedSecret,
'lottery.main_site.sso_jwt_secret' => 'different-legacy-sso-secret-32bytes!!',
]);
DB::table('admin_sites')->where('is_default', true)->update([
'status' => 1,
'sso_jwt_secret_encrypted' => encrypt($sharedSecret),
'updated_at' => now(),
]);
expectNativeSecretConfigurationRejected(
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
);
$token = nativeSecretGuardToken($player, $sharedSecret);
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/player/me')
->assertStatus(503)
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
});
test('native secret matching disabled database site sso secret rejects issuance and verification', function (): void {
$player = nativeSecretGuardPlayer('disabled-database-match');
$sharedSecret = 'shared-disabled-site-secret-32bytes!!';
config([
'lottery.player_auth.native.secret' => $sharedSecret,
'lottery.main_site.sso_jwt_secret' => 'different-legacy-sso-secret-32bytes!!',
]);
DB::table('admin_sites')->where('is_default', true)->update([
'status' => 0,
'sso_jwt_secret_encrypted' => encrypt($sharedSecret),
'updated_at' => now(),
]);
expectNativeSecretConfigurationRejected(
fn () => app(PlayerNativeAuthService::class)->issueToken($player),
);
$token = nativeSecretGuardToken($player, $sharedSecret);
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/player/me')
->assertStatus(503)
->assertJsonPath('code', ErrorCode::PlayerSsoSecretNotConfigured->value);
});
test('independent native secret still issues and verifies tokens', function (): void {
$player = nativeSecretGuardPlayer('independent');
config([
'lottery.player_auth.native.secret' => 'independent-native-secret-32bytes!!',
'lottery.main_site.sso_jwt_secret' => 'different-legacy-sso-secret-32bytes!!',
]);
DB::table('admin_sites')->where('is_default', true)->update([
'status' => 1,
'sso_jwt_secret_encrypted' => encrypt('different-database-sso-secret-32bytes!!'),
'updated_at' => now(),
]);
$token = app(PlayerNativeAuthService::class)->issueToken($player);
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/player/me')
->assertOk()
->assertJsonPath('data.id', $player->id)
->assertJsonPath('data.auth_source', PlayerAuthSource::LOTTERY_NATIVE);
});