fix(core): harden settlement and wallet integration
This commit is contained in:
174
tests/Feature/PlayerNativeJwtSecretGuardTest.php
Normal file
174
tests/Feature/PlayerNativeJwtSecretGuardTest.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?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);
|
||||
});
|
||||
Reference in New Issue
Block a user