Files
lotteryLaravel/tests/Feature/AdminSettingBatchUpdateTest.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

289 lines
10 KiB
PHP

<?php
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Models\LotterySetting;
use App\Services\LotterySettings;
use App\Support\SitePlatformRole;
use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function (): void {
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
});
function settingsAdminToken(): string
{
$admin = AdminUser::query()->create([
'username' => 'settings_editor',
'name' => 'Settings Editor',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
grantSuperAdminRole($admin);
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
function settingsReadOnlyToken(): string
{
$admin = AdminUser::query()->create([
'username' => 'settings_readonly',
'name' => 'Settings Readonly',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
$role = AdminRole::query()->create([
'slug' => 'settings_readonly_role',
'name' => 'Settings Readonly Role',
]);
$role->syncLegacyPermissionSlugs(['prd.rebate.manage']);
$admin->roles()->sync([
(int) $role->id => [
'site_id' => AdminUser::defaultAdminSiteId(),
'granted_at' => now(),
],
]);
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
function settingsSiteAdminToken(): string
{
$admin = AdminUser::query()->create([
'username' => 'settings_site_admin',
'name' => 'Settings Site Admin',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
$admin->roles()->sync([
SitePlatformRole::id() => [
'site_id' => AdminUser::defaultAdminSiteId(),
'granted_at' => now(),
],
]);
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
test('admin can batch update settings in one request', function (): void {
LotterySettings::put('draw.cooldown_minutes', 15, 'draw');
LotterySettings::put('draw.require_manual_review', true, 'draw');
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', [
'items' => [
['key' => 'draw.cooldown_minutes', 'value' => 20],
['key' => 'draw.require_manual_review', 'value' => false],
],
])
->assertOk()
->assertJsonPath('data.items.0.key', 'draw.cooldown_minutes');
expect(LotterySetting::query()->where('setting_key', 'draw.cooldown_minutes')->value('value_json'))->toBe(20)
->and(LotterySetting::query()->where('setting_key', 'draw.require_manual_review')->value('value_json'))->toBeFalse();
});
test('admin cannot batch update env locked deployment settings', function (): void {
config(['lottery.draw.interval_minutes' => 5]);
LotterySettings::put('draw.interval_minutes', 5, 'draw');
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', [
'items' => [
['key' => 'draw.interval_minutes', 'value' => 10],
],
])
->assertForbidden();
expect(LotterySettings::drawIntervalMinutes())->toBe(5);
});
test('admin settings deployment returns read only snapshot', function (): void {
config([
'lottery.default_currency' => 'USD',
'lottery.draw.interval_minutes' => 7,
]);
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/settings/deployment')
->assertOk()
->assertJsonPath('data.items.0.key', 'currency.default_code')
->assertJsonPath('data.items.0.value', 'USD')
->assertJsonPath('data.items.0.env_var', 'LOTTERY_DEFAULT_CURRENCY')
->assertJsonFragment(['key' => 'draw.interval_minutes', 'value' => 7]);
});
test('site admin cannot access system settings endpoints', function (): void {
LotterySettings::put('frontend.play_rules_html_zh', '<div>old</div>', 'frontend');
$token = settingsSiteAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/settings')
->assertForbidden();
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/settings/deployment')
->assertForbidden();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/frontend.play_rules_html_zh', [
'value' => '<div>new</div>',
])
->assertForbidden();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', [
'items' => [
['key' => 'frontend.play_rules_html_zh', 'value' => '<div>new</div>'],
],
])
->assertForbidden();
expect(LotterySetting::query()->where('setting_key', 'frontend.play_rules_html_zh')->value('value_json'))->toBe('<div>old</div>');
});
test('admin settings batch update rejects empty items', function (): void {
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', ['items' => []])
->assertUnprocessable();
});
test('admin can batch update settings with false and empty string values', function (): void {
LotterySettings::put('settlement.auto_run_on_tick', true, 'settlement');
LotterySettings::put('frontend.play_rules_html_zh', '<div>old</div>', 'frontend');
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', [
'items' => [
['key' => 'settlement.auto_run_on_tick', 'value' => false],
['key' => 'frontend.play_rules_html_zh', 'value' => ''],
],
])
->assertOk()
->assertJsonPath('data.items.0.key', 'frontend.play_rules_html_zh')
->assertJsonPath('data.items.0.value', '')
->assertJsonPath('data.items.1.key', 'settlement.auto_run_on_tick')
->assertJsonPath('data.items.1.value', false);
expect(LotterySetting::query()->where('setting_key', 'settlement.auto_run_on_tick')->value('value_json'))->toBeFalse()
->and(LotterySetting::query()->where('setting_key', 'frontend.play_rules_html_zh')->value('value_json'))->toBe('');
});
test('admin can update single setting with false value', function (): void {
LotterySettings::put('settlement.apply_rebate_to_payout', true, 'settlement');
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/settlement.apply_rebate_to_payout', [
'value' => false,
])
->assertOk()
->assertJsonPath('data.key', 'settlement.apply_rebate_to_payout')
->assertJsonPath('data.value', false);
expect(LotterySetting::query()->where('setting_key', 'settlement.apply_rebate_to_payout')->value('value_json'))->toBeFalse();
});
test('non payout manager cannot batch update settlement settings', function (): void {
LotterySettings::put('settlement.auto_payout_on_tick', true, 'settlement');
$token = settingsReadOnlyToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', [
'items' => [
['key' => 'settlement.auto_payout_on_tick', 'value' => false],
],
])
->assertForbidden();
expect(LotterySetting::query()->where('setting_key', 'settlement.auto_payout_on_tick')->value('value_json'))->toBeTrue();
});
test('non payout manager cannot update single settlement setting', function (): void {
LotterySettings::put('settlement.auto_approve_on_tick', true, 'settlement');
$token = settingsReadOnlyToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/settlement.auto_approve_on_tick', [
'value' => false,
])
->assertForbidden();
expect(LotterySetting::query()->where('setting_key', 'settlement.auto_approve_on_tick')->value('value_json'))->toBeTrue();
});
test('wallet limits must be positive integer minor units', function (): void {
LotterySettings::put('wallet.transfer_in_min_minor', 100, 'wallet');
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/batch', [
'items' => [
['key' => 'wallet.transfer_in_min_minor', 'value' => 0],
],
])
->assertUnprocessable();
expect(LotterySetting::query()->where('setting_key', 'wallet.transfer_in_min_minor')->value('value_json'))->toBe(100);
});
test('wallet max limit cannot be less than current min limit', function (): void {
LotterySettings::put('wallet.transfer_in_min_minor', 100, 'wallet');
LotterySettings::put('wallet.transfer_in_max_minor', 10_000, 'wallet');
$token = settingsAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/wallet.transfer_in_max_minor', [
'value' => 50,
])
->assertUnprocessable();
expect(LotterySetting::query()->where('setting_key', 'wallet.transfer_in_max_minor')->value('value_json'))->toBe(10_000);
});
test('non super admin cannot update settings even with setting related permissions', function (): void {
LotterySettings::put('draw.cooldown_minutes', 15, 'draw');
LotterySettings::put('frontend.play_rules_html_zh', '<div>old</div>', 'frontend');
$token = settingsReadOnlyToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/draw.cooldown_minutes', [
'value' => 10,
])
->assertForbidden();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/settings/frontend.play_rules_html_zh', [
'value' => '<div>new</div>',
])
->assertForbidden();
expect(LotterySetting::query()->where('setting_key', 'draw.cooldown_minutes')->value('value_json'))->toBe(15)
->and(LotterySetting::query()->where('setting_key', 'frontend.play_rules_html_zh')->value('value_json'))->toBe('<div>old</div>');
});