Add the period balance reconciliation page, permissions, financial closing balances, Jushuitan snapshots for Taobao Alipay, Pinduoduo and Douyin, and explicit unavailable states.
457 lines
21 KiB
PHP
457 lines
21 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\SystemSettings;
|
|
|
|
use App\Models\Auth\DingTalkIdentity;
|
|
use App\Models\Auth\Permission;
|
|
use App\Models\Auth\Role;
|
|
use App\Models\Auth\User;
|
|
use App\Utilities\AgreementSigningPermissions;
|
|
use App\Utilities\BusinessOperationPermissions;
|
|
use App\Utilities\BusinessNavigation;
|
|
use App\Utilities\SidebarPermissions;
|
|
use App\Utilities\SystemAccess;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class UserPermissionsTest extends FeatureTestCase
|
|
{
|
|
public function test_administrator_can_grant_and_revoke_signing_without_changing_business_user_role(): void
|
|
{
|
|
$admin = $this->account('admin');
|
|
$target = $this->account('user');
|
|
$this->loginAs($admin)->get(route('system-settings.users.edit', $target->id))->assertOk()
|
|
->assertSee('name="agreement_signing_allowed"', false);
|
|
foreach ([true, false] as $allowed) {
|
|
$this->patch(route('system-settings.users.update', $target->id),
|
|
$this->payload($target, 'user') + ['agreement_signing_allowed' => (int) $allowed])->assertRedirect();
|
|
$this->assertSame($allowed, AgreementSigningPermissions::canOperate($target->fresh()));
|
|
$this->assertSame(['user'], $target->roles()->pluck('name')->all());
|
|
}
|
|
}
|
|
|
|
public function test_unrelated_account_edit_preserves_existing_signing_grant(): void
|
|
{
|
|
$target = $this->account('user');
|
|
AgreementSigningPermissions::setGranted($target, true);
|
|
$this->loginAs()->patch(route('system-settings.users.update', $target->id), $this->payload($target, 'user'))->assertRedirect();
|
|
$this->assertTrue(AgreementSigningPermissions::canOperate($target->fresh()));
|
|
}
|
|
|
|
public function test_admin_sees_all_company_accounts_but_cannot_edit_administrators(): void
|
|
{
|
|
$admin = $this->account('admin');
|
|
$regular = $this->account('user');
|
|
$outside = $this->account('user');
|
|
$outside->companies()->detach();
|
|
$this->loginAs($admin)->get(route('system-settings.users.index'))
|
|
->assertOk()->assertSeeText($this->user->name)->assertSeeText($admin->name)->assertSeeText($regular->name)
|
|
->assertDontSeeText($outside->name)
|
|
->assertDontSee(route('system-settings.users.edit', $this->user->id), false)
|
|
->assertSee(route('system-settings.users.edit', $regular->id), false);
|
|
$this->withExceptionHandling()->get(route('system-settings.users.edit', $this->user->id))->assertForbidden();
|
|
$this->patch(route('system-settings.users.update', $this->user->id), $this->payload($this->user, 'user'))->assertForbidden();
|
|
$this->delete(route('system-settings.users.destroy', $this->user->id))->assertForbidden();
|
|
}
|
|
|
|
public function test_super_admin_can_assign_both_administrator_roles(): void
|
|
{
|
|
$target = $this->account('user');
|
|
$this->loginAs()->get(route('system-settings.users.edit', $target->id))->assertOk()
|
|
->assertSeeText('主管理员(最高权限)')->assertSee('value="admin"', false);
|
|
foreach (['admin', 'super_admin'] as $role) {
|
|
$this->patch(route('system-settings.users.update', $target->id), $this->payload($target, $role))
|
|
->assertRedirect(route('system-settings.users.index'));
|
|
$target = $target->fresh();
|
|
$this->assertSame([$role], $target->roles()->pluck('name')->all());
|
|
}
|
|
setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$target->id], 'subjects' => ['user_'.$target->id => []]]);
|
|
$this->assertSame(BusinessNavigation::all(), SidebarPermissions::navigationFor($target));
|
|
$this->assertTrue(SystemAccess::isSuperAdmin($target));
|
|
$this->loginAs($target)->get(route('system-settings.users.index'))->assertOk();
|
|
$this->get(route('settings.navigation.manage'))->assertOk();
|
|
}
|
|
|
|
public function test_admin_cannot_promote_a_user_or_itself(): void
|
|
{
|
|
$admin = $this->account('admin');
|
|
$target = $this->account('user');
|
|
$this->withExceptionHandling()->loginAs($admin);
|
|
foreach (['admin', 'super_admin'] as $role) {
|
|
$this->patchJson(route('system-settings.users.update', $target->id), $this->payload($target, $role))
|
|
->assertUnprocessable()->assertJsonValidationErrors('role');
|
|
}
|
|
$this->patchJson(route('system-settings.users.update', $admin->id), $this->payload($admin, 'super_admin'))->assertForbidden();
|
|
$this->assertSame(['user'], $target->roles()->pluck('name')->all());
|
|
}
|
|
|
|
public function test_demotion_removes_cached_administrator_access(): void
|
|
{
|
|
$target = $this->account('super_admin');
|
|
$this->assertTrue($target->hasRole('super_admin'));
|
|
$this->assertTrue($target->can('update-auth-users'));
|
|
$this->loginAs()->patch(route('system-settings.users.update', $target->id), $this->payload($target, 'user'))
|
|
->assertRedirect(route('system-settings.users.index'));
|
|
$this->assertSame('user', SystemAccess::roleName($target));
|
|
$this->assertFalse($target->fresh()->can('update-auth-users'));
|
|
$this->withExceptionHandling()->loginAs($target)->get(route('system-settings.users.index'))->assertForbidden();
|
|
}
|
|
|
|
public function test_legacy_user_routes_cannot_grant_administrator_identity(): void
|
|
{
|
|
$admin = $this->account('admin');
|
|
$target = $this->account('user');
|
|
$this->withExceptionHandling()->loginAs($admin);
|
|
$role = (string) Role::where('name', 'super_admin')->value('id');
|
|
$this->patchJson(route('users.update', $target->id), ['roles' => $role])->assertForbidden();
|
|
$this->patchJson(route('profile.update', $admin->id), ['roles' => $role])->assertForbidden();
|
|
$this->postJson(route('users.store'), ['roles' => $role])->assertForbidden();
|
|
$this->patchJson(route('users.update', $this->user->id), ['name' => 'unauthorized'])->assertForbidden();
|
|
}
|
|
|
|
public function test_last_super_admin_cannot_be_demoted_or_disabled(): void
|
|
{
|
|
$this->loginAs();
|
|
$this->patch(route('system-settings.users.update', $this->user->id), $this->payload($this->user, 'admin'))
|
|
->assertSessionHasErrors('role');
|
|
$this->patch(route('system-settings.users.update', $this->user->id), array_merge($this->payload($this->user, 'super_admin'), ['enabled' => 0]))
|
|
->assertSessionHasErrors('enabled');
|
|
$this->assertSame(['super_admin'], $this->user->roles()->pluck('name')->all());
|
|
}
|
|
|
|
public function test_user_page_settings_are_a_second_level_page_and_save_for_selected_user(): void
|
|
{
|
|
$target = $this->account('user');
|
|
$this->loginAs()->get(route('system-settings.permissions.index'))->assertOk()
|
|
->assertSeeText('用户页面与操作权限')->assertSee(route('system-settings.sidebar.edit'), false);
|
|
$this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]))->assertOk()
|
|
->assertSee('aria-label="页面位置"', false)
|
|
->assertSeeText('主体商标规则')
|
|
->assertSeeText('退款待确认 · 导出明细')
|
|
->assertSee('value="fee-ratio-monitoring/refund-pending/export-details"', false);
|
|
$this->patch(route('system-settings.sidebar.update'), [
|
|
'subject' => 'user_'.$target->id, 'permissions' => ['store-management/basic-information'],
|
|
])->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]));
|
|
$this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($target));
|
|
$this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]))->assertOk()->assertSeeText('自定义权限');
|
|
}
|
|
|
|
public function test_posting_data_follows_store_receivables_permissions(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
|
|
foreach (['store-receivables', 'store-receivables/actual-reconciliation'] as $receivablePermission) {
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => [$receivablePermission]],
|
|
]);
|
|
|
|
$allowed = SidebarPermissions::allowedKeys($regular);
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
|
|
$this->assertContains('posting-data', $allowed);
|
|
$this->assertSame(['store-receivables', 'posting-data'], array_column($navigation, 'slug'));
|
|
$this->assertSame(
|
|
['kuka-posting', 'suofeiya-posting'],
|
|
array_column($navigation[1]['pages'], 'slug')
|
|
);
|
|
$this->assertSame(
|
|
['顾家过账', '索菲亚过账'],
|
|
array_column($navigation[1]['pages'], 'title')
|
|
);
|
|
$this->assertSame(
|
|
['kuka-basic-data', 'kuka-reconciliation'],
|
|
array_column($navigation[1]['pages'][0]['children'], 'slug')
|
|
);
|
|
}
|
|
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['posting-data/kuka-posting']],
|
|
]);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$this->assertSame(
|
|
['kuka-basic-data', 'kuka-reconciliation'],
|
|
array_column($navigation[0]['pages'][0]['children'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_period_balance_reconciliation_inherits_store_receiving_bills_permission(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_' . $regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['billing-transactions/store-receiving-bills']],
|
|
]);
|
|
|
|
$this->assertContains(
|
|
'billing-transactions/period-balance-reconciliation',
|
|
SidebarPermissions::allowedKeys($regular),
|
|
);
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$billing = collect($navigation)->firstWhere('slug', 'billing-transactions');
|
|
$pageSlugs = array_column($billing['pages'] ?? [], 'slug');
|
|
|
|
$this->assertContains('store-receiving-bills', $pageSlugs);
|
|
$this->assertContains('period-balance-reconciliation', $pageSlugs);
|
|
}
|
|
|
|
public function test_refund_export_requires_a_separate_operation_grant_when_page_access_is_limited(): void
|
|
{
|
|
$target = $this->account('user');
|
|
$subject = 'user_' . $target->id;
|
|
$pageKey = 'fee-ratio-monitoring/refund-pending';
|
|
$operationKey = $pageKey . '/export-details';
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => [$pageKey]],
|
|
]);
|
|
|
|
$this->assertTrue(SidebarPermissions::canViewPage($target, 'fee-ratio-monitoring', 'refund-pending'));
|
|
$this->assertFalse(BusinessOperationPermissions::can('fee-ratio-monitoring', 'refund-pending', 'export-details', $target));
|
|
|
|
$this->loginAs()->patch(route('system-settings.sidebar.update'), [
|
|
'subject' => $subject,
|
|
'permissions' => [$pageKey, $operationKey],
|
|
])->assertRedirect(route('system-settings.sidebar.edit', ['subject' => $subject]));
|
|
|
|
$this->assertTrue(BusinessOperationPermissions::can('fee-ratio-monitoring', 'refund-pending', 'export-details', $target));
|
|
$this->assertSame([$pageKey, $operationKey], SidebarPermissions::allowedKeys($target));
|
|
}
|
|
|
|
public function test_whole_refunds_section_grant_includes_refund_export(): void
|
|
{
|
|
$target = $this->account('user');
|
|
$subject = 'user_' . $target->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['refunds']],
|
|
]);
|
|
|
|
$this->assertTrue(BusinessOperationPermissions::can('fee-ratio-monitoring', 'refund-pending', 'export-details', $target));
|
|
}
|
|
|
|
public function test_super_admin_cannot_be_limited_by_page_settings_and_regular_users_cannot_configure_access(): void
|
|
{
|
|
$this->withExceptionHandling()->loginAs()->patchJson(route('system-settings.sidebar.update'), [
|
|
'subject' => 'user_'.$this->user->id, 'permissions' => [],
|
|
])->assertUnprocessable();
|
|
$regular = $this->account('user');
|
|
$this->loginAs($regular)->get(route('system-settings.permissions.index'))->assertForbidden();
|
|
$this->get(route('system-settings.users.index'))->assertForbidden();
|
|
$this->patchJson(route('system-settings.sidebar.update'), ['subject' => 'role_user', 'permissions' => []])->assertForbidden();
|
|
}
|
|
|
|
public function test_child_page_navigation_contains_only_the_selected_child(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$regular->id], 'subjects' => ['user_'.$regular->id => ['trademark-management/subject-trademark-rules']]]);
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$pages = collect($navigation)->flatMap(fn ($section) => $section['pages'])->flatMap(fn ($page) => $page['children'] ?? [$page]);
|
|
$this->assertSame(['subject-trademark-rules'], $pages->pluck('slug')->all());
|
|
}
|
|
|
|
public function test_financial_risk_control_section_permission_includes_store_violation_alert(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['financial-risk-control']],
|
|
]);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
|
|
$this->assertSame(['financial-risk-control'], array_column($navigation, 'slug'));
|
|
$this->assertSame(
|
|
['store-violation-alert', 'process-statistics'],
|
|
array_column($navigation[0]['pages'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_expense_details_follows_existing_financial_page_permissions(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => [
|
|
'financial-operations/performance-details',
|
|
'financial-operations/billing-details',
|
|
]],
|
|
]);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
|
|
$this->assertSame(['financial-operations'], array_column($navigation, 'slug'));
|
|
$this->assertSame(
|
|
['performance-details', 'expense-details', 'billing-details'],
|
|
array_column($navigation[0]['pages'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_expense_details_replaces_legacy_standalone_expense_entries(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['financial-operations/advertising-expenses']],
|
|
]);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
|
|
$this->assertSame(['financial-operations'], array_column($navigation, 'slug'));
|
|
$this->assertSame(['expense-details'], array_column($navigation[0]['pages'], 'slug'));
|
|
}
|
|
|
|
public function test_process_statistics_follows_the_violation_alert_page_permission(): void
|
|
{
|
|
// 只按页面键授权过的账号(旧配置)也要能看到新增的「流程统计」,与店铺违规预警同权限。
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['financial-risk-control/store-violation-alert']],
|
|
]);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
|
|
$this->assertSame(['financial-risk-control'], array_column($navigation, 'slug'));
|
|
$this->assertSame(
|
|
['store-violation-alert', 'process-statistics'],
|
|
array_column($navigation[0]['pages'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_legacy_small_payment_permission_follows_the_page_to_financial_operations(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['refunds/small-payment-work-orders']],
|
|
]);
|
|
|
|
$this->assertSame(
|
|
['financial-operations/small-payment-work-orders'],
|
|
SidebarPermissions::allowedKeys($regular)
|
|
);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$this->assertSame(['financial-operations'], array_column($navigation, 'slug'));
|
|
$this->assertSame(
|
|
['small-payment-work-orders'],
|
|
array_column($navigation[0]['pages'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_legacy_refunds_section_permission_keeps_small_payment_access(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_'.$regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['refunds']],
|
|
]);
|
|
|
|
$this->assertSame(
|
|
['refunds', 'financial-operations/small-payment-work-orders'],
|
|
SidebarPermissions::allowedKeys($regular)
|
|
);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$this->assertSame(
|
|
['refunds', 'financial-operations'],
|
|
array_column($navigation, 'slug')
|
|
);
|
|
$this->assertSame(
|
|
['small-payment-work-orders'],
|
|
array_column($navigation[1]['pages'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_payables_final_entries_follow_their_base_section_permissions(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_' . $regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['supplier-payables', 'courier-payables']],
|
|
]);
|
|
|
|
$this->assertEqualsCanonicalizing(
|
|
['supplier-payables', 'supplier-payables-final', 'courier-payables', 'courier-payables-final'],
|
|
SidebarPermissions::allowedKeys($regular)
|
|
);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$this->assertSame(
|
|
['supplier-payables-final', 'courier-payables-final', 'supplier-payables', 'courier-payables'],
|
|
array_column($navigation, 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_payables_final_entry_follows_a_selected_child_permission(): void
|
|
{
|
|
$regular = $this->account('user');
|
|
$subject = 'user_' . $regular->id;
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => [$subject],
|
|
'subjects' => [$subject => ['supplier-payables/product-prices']],
|
|
]);
|
|
|
|
$this->assertSame(
|
|
['supplier-payables/product-prices', 'supplier-payables-final/product-prices'],
|
|
SidebarPermissions::allowedKeys($regular)
|
|
);
|
|
|
|
$navigation = SidebarPermissions::navigationFor($regular);
|
|
$this->assertSame(
|
|
['supplier-payables-final', 'supplier-payables'],
|
|
array_column($navigation, 'slug')
|
|
);
|
|
$this->assertSame(
|
|
['product-prices'],
|
|
array_column($navigation[0]['pages'], 'slug')
|
|
);
|
|
$this->assertSame(
|
|
['product-prices'],
|
|
array_column($navigation[1]['pages'], 'slug')
|
|
);
|
|
}
|
|
|
|
public function test_dingtalk_bound_user_cannot_be_deleted_into_an_orphaned_identity(): void
|
|
{
|
|
$target = $this->account('user');
|
|
DingTalkIdentity::query()->create([
|
|
'user_id' => $target->id,
|
|
'union_id' => 'fixture-delete-guard-union',
|
|
'dingtalk_user_id' => 'fixture-delete-guard-user',
|
|
'nickname' => '在职财务',
|
|
]);
|
|
|
|
$this->loginAs()->delete(route('system-settings.users.destroy', $target->id))
|
|
->assertSessionHasErrors('user');
|
|
$this->assertNotNull($target->fresh());
|
|
$this->assertDatabaseHas('dingtalk_identities', ['user_id' => $target->id]);
|
|
}
|
|
|
|
private function account(string $role): User
|
|
{
|
|
$name = 'fixture-'.$role.'-'.User::count();
|
|
$account = User::create(['name' => $name, 'email' => $name.'@example.test', 'password' => 'fixture-unused-password', 'enabled' => true, 'locale' => 'zh-CN', 'landing_page' => 'dashboard']);
|
|
$account->roles()->attach(Role::where('name', $role)->firstOrFail()->id);
|
|
$account->permissions()->attach(Permission::where('name', 'read-admin-panel')->firstOrFail()->id);
|
|
$account->companies()->attach($this->company->id);
|
|
return $account;
|
|
}
|
|
|
|
private function payload(User $target, string $role): array
|
|
{
|
|
return ['name' => $target->name, 'email' => $target->email, 'role' => $role, 'enabled' => 1];
|
|
}
|
|
}
|