getJson('/api/v1/admin/ping')->assertUnauthorized() ->assertJsonPath('code', ErrorCode::AdminUnauthenticated->value); }); test('admin auth me returns current admin profile', function () { $admin = AdminUser::query()->create([ 'username' => 'admin_me', 'name' => '管理员本人', 'email' => null, 'password' => 'secret-strong', 'status' => 0, ]); $roleId = DB::table('admin_roles')->insertGetId([ 'code' => 'super_admin', 'slug' => 'super_admin', 'name' => '超级管理员', 'description' => null, 'status' => 1, 'is_system' => true, 'sort_order' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $siteId = DB::table('admin_sites')->insertGetId([ 'code' => 'default', 'name' => '默认站点', 'is_default' => true, 'status' => 1, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $admin->id, 'site_id' => $siteId, 'role_id' => $roleId, 'granted_at' => now(), ]); $token = $admin->createToken('admin-api', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/auth/me') ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonPath('data.admin.username', 'admin_me') ->assertJsonPath('data.admin.navigation.0.segment', 'dashboard') ->assertJsonStructure(['data' => ['admin' => ['permissions', 'operational_permissions']]]); }); test('admin login returns bearer token when captcha passes validation', function () { $admin = AdminUser::query()->create([ 'username' => 'tester', 'name' => '测试昵称', 'email' => null, 'password' => 'secret-strong', 'status' => 0, ]); // 登录返回的 navigation 需要管理员权限(避免测试依赖默认 DB 状态) grantSuperAdminRole($admin); $captchaKey = (string) Str::uuid(); Cache::put( 'admin_captcha:'.$captchaKey, hash_hmac('sha256', 'xwz2', (string) config('app.key')), now()->addSeconds(120), ); $resp = $this->postJson('/api/v1/admin/auth/login', [ 'account' => 'Tester', 'password' => 'secret-strong', 'captcha_key' => $captchaKey, 'captcha_code' => 'xwz2', ]); $resp->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonPath('data.admin.username', 'tester') ->assertJsonPath('data.admin.nickname', '测试昵称') ->assertJsonPath('data.admin.navigation.0.segment', 'dashboard') ->assertJsonPath('data.admin.navigation.0.href', '/admin') ->assertJsonPath('data.admin.navigation.0.nav_group', 'overview') ->assertJsonPath('data.admin.navigation.1.segment', 'agents') ->assertJsonPath('data.admin.navigation.1.nav_group', 'agent') ->assertJsonPath('data.admin.navigation.2.segment', 'draws') ->assertJsonStructure(['data' => ['token', 'token_type', 'admin' => ['id', 'username', 'nickname', 'email', 'permissions', 'operational_permissions', 'navigation']]]); $token = $resp->json('data.token'); expect($token)->not->toBeNull(); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/ping') ->assertOk() ->assertJsonPath('data.scope', 'admin'); }); test('agent operator auth me omits platform-only navigation', function (): void { $this->artisan('lottery:admin-auth-sync')->assertExitCode(0); $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $admin = AdminUser::query()->create([ 'username' => 'agent_nav_ops', 'name' => 'Agent Nav', 'email' => null, 'password' => 'secret-strong', 'status' => 0, ]); $now = now(); $roleId = DB::table('admin_roles')->insertGetId([ 'slug' => 'agent_nav_ops_role', 'code' => 'agent_nav_ops_role', 'name' => 'Agent Nav Ops', 'description' => null, 'status' => 1, 'is_system' => false, 'sort_order' => 0, 'created_at' => $now, 'updated_at' => $now, ]); $codes = ['agent.node.view', 'service.report.view']; $actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id'); foreach ($actionIds as $actionId) { DB::table('admin_role_menu_actions')->insert([ 'role_id' => $roleId, 'menu_action_id' => (int) $actionId, ]); } DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $admin->id, 'site_id' => $siteId, 'role_id' => $roleId, 'granted_at' => $now, ]); DB::table('admin_user_agents')->insert([ 'admin_user_id' => $admin->id, 'agent_node_id' => $rootId, 'is_primary' => true, 'granted_at' => $now, ]); $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; $segments = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/auth/me') ->assertOk() ->json('data.admin.navigation'); $keys = array_column($segments, 'segment'); expect($keys)->toContain('agents', 'reports') ->and($keys)->not->toContain('admin_users', 'admin_roles', 'settings', 'integration', 'rules_plays'); }); test('admin captcha exposes key and image base64', function () { $resp = $this->getJson('/api/v1/admin/auth/captcha'); $resp->assertOk() ->assertJsonPath('code', ErrorCode::Success->value); $data = $resp->json('data'); expect($data)->toBeArray() ->and(Str::isUuid((string) $data['captcha_key']))->toBeTrue() ->and((string) $data['image_base64'])->not->toBe(''); }); test('login rejects wrong password with masked message', function () { AdminUser::query()->create([ 'username' => 'bad_tester', 'name' => 'X', 'email' => null, 'password' => 'right-only', 'status' => 0, ]); $captchaKey = (string) Str::uuid(); Cache::put( 'admin_captcha:'.$captchaKey, hash_hmac('sha256', 'aaaa', (string) config('app.key')), now()->addSeconds(120), ); $this->postJson('/api/v1/admin/auth/login', [ 'account' => 'bad_tester', 'password' => 'wrong-password', 'captcha_key' => $captchaKey, 'captcha_code' => 'aaaa', ])->assertUnauthorized() ->assertJsonPath('code', ErrorCode::AdminCredentialsInvalid->value); });