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