create([ 'site_code' => 'main', 'site_player_id' => 'uid-42', 'username' => 'alice', 'nickname' => 'A', 'default_currency' => 'NPR', 'status' => 0, ]); $this->withHeaders([ 'Authorization' => 'Bearer dev:'.$player->id, 'X-Locale' => 'zh', ]) ->getJson('/api/v1/player/me') ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonPath('data.id', $player->id) ->assertJsonPath('data.site_player_id', 'uid-42') ->assertJsonPath('data.username', 'alice') ->assertJsonPath('data.locale', 'zh') ->assertJsonStructure([ 'data' => [ 'last_login_at', 'created_at', ], ]); $player->refresh(); expect($player->last_login_at)->not->toBeNull(); }); test('player auth missing bearer returns localized sso 8001', function () { $code = ErrorCode::PlayerAuthorizationInvalid->value; $this->withHeader('Accept-Language', 'zh-CN,zh;q=0.9') ->getJson('/api/v1/player/me') ->assertStatus(Response::HTTP_UNAUTHORIZED) ->assertJsonPath('code', $code) ->assertJsonPath('msg', __("sso.$code", [], 'zh')); }); test('api unknown route returns unified not_found json without hitting locale middleware', function () { $this->withHeader('X-Locale', 'zh') ->getJson('/api/v1/player/__no_route__xxx') ->assertStatus(Response::HTTP_NOT_FOUND) ->assertJsonPath('code', ErrorCode::NotFound->value) ->assertJsonPath('msg', __('api.not_found', [], 'zh')); }); test('player me works with main site jwt when dev bypass is off', function () { config(['lottery.player_auth.dev_bypass' => false]); config(['lottery.main_site.sso_jwt_secret' => 'jwt-test-secret']); $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'jwt-user-1', 'username' => null, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); $jwt = JWT::encode([ 'site_code' => 'main', 'site_player_id' => 'jwt-user-1', 'exp' => time() + 3600, ], 'jwt-test-secret', 'HS256'); $this->withHeader('Authorization', 'Bearer '.$jwt) ->getJson('/api/v1/player/me') ->assertOk() ->assertJsonPath('data.site_player_id', 'jwt-user-1'); }); test('jwt first successful login auto-registers player mapping', function () { config(['lottery.player_auth.dev_bypass' => false]); config(['lottery.main_site.sso_jwt_secret' => 'jwt-test-secret']); expect(Player::query()->count())->toBe(0); $jwt = JWT::encode([ 'site_code' => 'main', 'site_player_id' => 'brand-new-sso-1', 'exp' => time() + 3600, ], 'jwt-test-secret', 'HS256'); $this->withHeader('Authorization', 'Bearer '.$jwt) ->getJson('/api/v1/player/me') ->assertOk() ->assertJsonPath('data.site_player_id', 'brand-new-sso-1') ->assertJsonPath('data.default_currency', 'NPR'); expect(Player::query()->where('site_player_id', 'brand-new-sso-1')->count())->toBe(1); }); test('player me rejects non-active status with 8005', function () { $code = ErrorCode::PlayerAccountSuspended->value; $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'frozen-1', 'username' => null, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 1, ]); $this->withHeaders([ 'Authorization' => 'Bearer dev:'.$player->id, 'Accept-Language' => 'zh-CN,zh;q=0.9', ]) ->getJson('/api/v1/player/me') ->assertStatus(403) ->assertJsonPath('code', $code) ->assertJsonPath('msg', __("sso.$code", [], 'zh')); });