- 添加ensureSuperAdmin方法,限制管理员设置操作权限 - 在AdminSettingController接口中新增权限检查,防止非超级管理员操作 - AdminPlayerIndexController新增direct agent筛选支持 - AdminPlayerUpdateController新增对信用玩家默认币别变更的拒绝逻辑 - 新增WalletSettlementBillsController,实现玩家信用盘账期账单摘要接口 - AgentProfileService调整,新增返点限额和分享比例自动下调机制,保持子代理及玩家配置不超父级 - AgentProfileService增加can_grant_extra_rebate权限继承限制,阻止无权限代理开启 - AgentSettlementPeriodCloseService增加玩家账单回水信用释放逻辑,确保信用额度同步 - PlayerCreditService新增释放账单回水对应信用逻辑,维护账期信用一致性 - TicketPlacementService和TicketPreviewService新增信用玩家投注币别匹配校验,防止币别不符 - PlayerLedgerLogsService优化信用额度计算,增加可用额度上下限限制,防止负值和超限 - 调整后台管理导航,仅超管可见设置入口,强化权限隔离 - 路由新增玩家信用账单查询接口 - 补充多项AgentProfile相关单元测试覆盖额度限制、返点继承、返点下调场景及权限限制 - 增加站点管理员登录测试,验证系统设置菜单不可见,提升用户权限体验
289 lines
10 KiB
PHP
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>');
|
|
});
|