Files
lotteryLaravel/tests/Feature/AdminAuthLoginTest.php
kang 992195b00c
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
feat(agent-profile): 限制代理及玩家返点和分成范围,增强权限校验
- 添加ensureSuperAdmin方法,限制管理员设置操作权限
- 在AdminSettingController接口中新增权限检查,防止非超级管理员操作
- AdminPlayerIndexController新增direct agent筛选支持
- AdminPlayerUpdateController新增对信用玩家默认币别变更的拒绝逻辑
- 新增WalletSettlementBillsController,实现玩家信用盘账期账单摘要接口
- AgentProfileService调整,新增返点限额和分享比例自动下调机制,保持子代理及玩家配置不超父级
- AgentProfileService增加can_grant_extra_rebate权限继承限制,阻止无权限代理开启
- AgentSettlementPeriodCloseService增加玩家账单回水信用释放逻辑,确保信用额度同步
- PlayerCreditService新增释放账单回水对应信用逻辑,维护账期信用一致性
- TicketPlacementService和TicketPreviewService新增信用玩家投注币别匹配校验,防止币别不符
- PlayerLedgerLogsService优化信用额度计算,增加可用额度上下限限制,防止负值和超限
- 调整后台管理导航,仅超管可见设置入口,强化权限隔离
- 路由新增玩家信用账单查询接口
- 补充多项AgentProfile相关单元测试覆盖额度限制、返点继承、返点下调场景及权限限制
- 增加站点管理员登录测试,验证系统设置菜单不可见,提升用户权限体验
2026-07-01 15:35:46 +08:00

222 lines
7.8 KiB
PHP

<?php
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use App\Support\SitePlatformRole;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('admin ping requires authentication', function () {
$this->getJson('/api/v1/admin/ping')->assertUnauthorized()
->assertJsonPath('code', ErrorCode::AdminUnauthenticated->value);
});
test('admin auth me returns current admin profile', function () {
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
$admin = AdminUser::query()->create([
'username' => 'admin_me',
'name' => '管理员本人',
'email' => null,
'password' => 'secret-strong',
'status' => 0,
'is_super_admin' => true,
]);
$token = $admin->createToken('admin-api', ['*'], now()->addDay())->plainTextToken;
$resp = $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.account_kind', 'super_admin')
->assertJsonPath('data.admin.navigation.0.segment', 'dashboard')
->assertJsonStructure(['data' => ['admin' => ['permissions', 'operational_permissions']]]);
$permissions = $resp->json('data.admin.permissions');
$operational = $resp->json('data.admin.operational_permissions');
expect($permissions)->toBeArray()->not->toBeEmpty()
->and($operational)->toBeArray()->not->toBeEmpty()
->and($permissions[0])->toStartWith('prd.')
->and($operational[0])->not->toStartWith('prd.');
});
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', 'settlement_center')
->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('site admin auth me omits system settings navigation', function (): void {
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
$admin = AdminUser::query()->create([
'username' => 'site_admin_nav',
'name' => 'Site Admin Nav',
'email' => null,
'password' => 'secret-strong',
'status' => 0,
]);
$admin->roles()->sync([
SitePlatformRole::id() => [
'site_id' => AdminUser::defaultAdminSiteId(),
'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('dashboard', 'agents', 'players', 'wallet')
->and($keys)->not->toContain('settings', 'admin_users', 'admin_roles', '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);
});