fix(core): harden settlement and wallet integration
This commit is contained in:
234
tests/Feature/AdminReportJobScopeTest.php
Normal file
234
tests/Feature/AdminReportJobScopeTest.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\ReportJob;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use App\Services\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param list<string> $permissionCodes
|
||||
*/
|
||||
function makeReportScopeAdmin(string $username, int $siteId, array $permissionCodes): AdminUser
|
||||
{
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => $username,
|
||||
'name' => $username,
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$role = AdminRole::query()->create([
|
||||
'slug' => 'report_scope_'.$username,
|
||||
'code' => 'report_scope_'.$username,
|
||||
'name' => 'Report Scope '.$username,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
'status' => 1,
|
||||
'is_system' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$actionIds = DB::table('admin_menu_actions')
|
||||
->whereIn('permission_code', $permissionCodes)
|
||||
->pluck('id');
|
||||
|
||||
foreach ($actionIds as $actionId) {
|
||||
DB::table('admin_role_menu_actions')->insert([
|
||||
'role_id' => (int) $role->id,
|
||||
'menu_action_id' => (int) $actionId,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('admin_user_site_roles')->insert([
|
||||
'admin_user_id' => (int) $admin->id,
|
||||
'site_id' => $siteId,
|
||||
'role_id' => (int) $role->id,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
function makeReportScopeJob(AdminUser $owner, string $jobNo, string $reportType = 'daily_profit_summary'): ReportJob
|
||||
{
|
||||
return ReportJob::query()->create([
|
||||
'job_no' => $jobNo,
|
||||
'admin_user_id' => (int) $owner->id,
|
||||
'report_type' => $reportType,
|
||||
'export_format' => 'csv',
|
||||
'filter_json' => [
|
||||
'date_from' => now()->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
],
|
||||
'status' => 'completed',
|
||||
'output_path' => 'reports/'.$jobNo.'.csv',
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
function makeReportScopeSuperAdmin(): AdminUser
|
||||
{
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'report_scope_super',
|
||||
'name' => 'Report Scope Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
test('report job list show and download are owner only with an explicit super admin exception', function (): void {
|
||||
$siteAId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$siteB = AdminSite::query()->create([
|
||||
'code' => 'report-scope-b',
|
||||
'name' => 'Report Scope B',
|
||||
'currency_code' => 'NPR',
|
||||
'status' => 1,
|
||||
'is_default' => false,
|
||||
]);
|
||||
$permissions = ['service.report.view', 'service.report.export'];
|
||||
$owner = makeReportScopeAdmin('report_owner', $siteAId, $permissions);
|
||||
$sameSitePeer = makeReportScopeAdmin('report_same_site_peer', $siteAId, $permissions);
|
||||
$crossSitePeer = makeReportScopeAdmin('report_cross_site_peer', (int) $siteB->id, $permissions);
|
||||
|
||||
$ownedJob = makeReportScopeJob($owner, 'RPT-SCOPE-OWN');
|
||||
$sameSiteJob = makeReportScopeJob($sameSitePeer, 'RPT-SCOPE-SAME');
|
||||
$crossSiteJob = makeReportScopeJob($crossSitePeer, 'RPT-SCOPE-CROSS');
|
||||
|
||||
$deletedCreator = makeReportScopeAdmin('report_deleted_creator', $siteAId, $permissions);
|
||||
$orphanedJob = makeReportScopeJob($deletedCreator, 'RPT-SCOPE-ORPHAN');
|
||||
$deletedCreator->delete();
|
||||
$orphanedJob->refresh();
|
||||
expect($orphanedJob->admin_user_id)->toBeNull();
|
||||
|
||||
Sanctum::actingAs($owner, ['*']);
|
||||
$visibleIds = collect($this->getJson('/api/v1/admin/report-jobs')
|
||||
->assertOk()
|
||||
->json('data.items'))
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
expect($visibleIds)->toBe([(int) $ownedJob->id]);
|
||||
|
||||
$this->getJson('/api/v1/admin/report-jobs/'.$ownedJob->id)->assertOk();
|
||||
$this->get('/api/v1/admin/report-jobs/'.$ownedJob->id.'/download')->assertOk();
|
||||
|
||||
foreach ([$sameSiteJob, $crossSiteJob, $orphanedJob] as $deniedJob) {
|
||||
$this->getJson('/api/v1/admin/report-jobs/'.$deniedJob->id)->assertForbidden();
|
||||
$this->get('/api/v1/admin/report-jobs/'.$deniedJob->id.'/download')->assertForbidden();
|
||||
}
|
||||
|
||||
$super = makeReportScopeSuperAdmin();
|
||||
Sanctum::actingAs($super, ['*']);
|
||||
$superVisibleIds = collect($this->getJson('/api/v1/admin/report-jobs')
|
||||
->assertOk()
|
||||
->json('data.items'))
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
expect($superVisibleIds)->toContain(
|
||||
(int) $ownedJob->id,
|
||||
(int) $sameSiteJob->id,
|
||||
(int) $crossSiteJob->id,
|
||||
(int) $orphanedJob->id,
|
||||
);
|
||||
|
||||
$this->getJson('/api/v1/admin/report-jobs/'.$orphanedJob->id)->assertOk();
|
||||
$this->get('/api/v1/admin/report-jobs/'.$orphanedJob->id.'/download')->assertOk();
|
||||
});
|
||||
|
||||
test('sensitive report types require their own capabilities and risk export is super admin only', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$base = makeReportScopeAdmin('report_base_exporter', $siteId, [
|
||||
'service.report.view',
|
||||
'service.report.export',
|
||||
]);
|
||||
$auditor = makeReportScopeAdmin('report_auditor', $siteId, [
|
||||
'service.report.view',
|
||||
'service.report.export',
|
||||
'service.audit.view',
|
||||
]);
|
||||
$riskViewer = makeReportScopeAdmin('report_risk_viewer', $siteId, [
|
||||
'service.report.view',
|
||||
'service.report.export',
|
||||
'risk.monitor.view',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($base, ['*']);
|
||||
$this->postJson('/api/v1/admin/report-jobs', [
|
||||
'report_type' => 'audit_operation_report',
|
||||
])->assertForbidden();
|
||||
$legacyAuditJob = makeReportScopeJob($base, 'RPT-SENSITIVE-AUDIT', 'audit_operation_report');
|
||||
$this->get('/api/v1/admin/report-jobs/'.$legacyAuditJob->id.'/download')->assertForbidden();
|
||||
|
||||
Sanctum::actingAs($auditor, ['*']);
|
||||
$this->postJson('/api/v1/admin/report-jobs', [
|
||||
'report_type' => 'audit_operation_report',
|
||||
])->assertOk();
|
||||
|
||||
Sanctum::actingAs($riskViewer, ['*']);
|
||||
$legacyRiskJob = makeReportScopeJob($riskViewer, 'RPT-SENSITIVE-RISK', 'hot_number_risk_report');
|
||||
foreach (['hot_number_risk_report', 'sold_out_number_report'] as $reportType) {
|
||||
$this->postJson('/api/v1/admin/report-jobs', [
|
||||
'report_type' => $reportType,
|
||||
])->assertForbidden();
|
||||
}
|
||||
$this->get('/api/v1/admin/report-jobs/'.$legacyRiskJob->id.'/download')->assertForbidden();
|
||||
|
||||
$super = makeReportScopeSuperAdmin();
|
||||
Sanctum::actingAs($super, ['*']);
|
||||
foreach (['hot_number_risk_report', 'sold_out_number_report'] as $reportType) {
|
||||
$created = $this->postJson('/api/v1/admin/report-jobs', [
|
||||
'report_type' => $reportType,
|
||||
])->assertOk();
|
||||
$this->get('/api/v1/admin/report-jobs/'.(int) $created->json('data.id').'/download')->assertOk();
|
||||
}
|
||||
});
|
||||
|
||||
test('non super audit export is limited to the current actor', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$auditor = makeReportScopeAdmin('report_audit_owner', $siteId, [
|
||||
'service.report.view',
|
||||
'service.report.export',
|
||||
'service.audit.view',
|
||||
]);
|
||||
$other = makeReportScopeAdmin('report_audit_other', $siteId, [
|
||||
'service.report.view',
|
||||
'service.report.export',
|
||||
'service.audit.view',
|
||||
]);
|
||||
|
||||
AuditLogger::recordForAdmin($auditor, null, 'audit_scope_own', 'own_action', null, null, null, null);
|
||||
AuditLogger::recordForAdmin($other, null, 'audit_scope_other', 'other_action', null, null, null, null);
|
||||
|
||||
Sanctum::actingAs($auditor, ['*']);
|
||||
$create = $this->postJson('/api/v1/admin/report-jobs', [
|
||||
'report_type' => 'audit_operation_report',
|
||||
'export_format' => 'csv',
|
||||
'parameters' => [
|
||||
'date_from' => now()->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$content = $this->get('/api/v1/admin/report-jobs/'.(int) $create->json('data.id').'/download')
|
||||
->assertOk()
|
||||
->streamedContent();
|
||||
|
||||
expect($content)->toContain('audit_scope_own')
|
||||
->not->toContain('audit_scope_other');
|
||||
});
|
||||
Reference in New Issue
Block a user