Files
portal/app/tests/Feature/Reports/ReportDownloadTest.php
T

157 lines
5.7 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\ReportJob;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
Storage::fake('local');
});
function doneReportJob(int $tenantId, int $userId, array $overrides = []): ReportJob
{
return ReportJob::create(array_merge([
'tenant_id' => $tenantId,
'user_id' => $userId,
'type' => 'deals_export',
'parameters' => ['format' => 'csv', 'date_from' => '2026-04-01', 'date_to' => '2026-04-30'],
'status' => ReportJob::STATUS_DONE,
'file_path' => null,
'file_size' => null,
'finished_at' => Carbon::now(),
'expires_at' => Carbon::now()->addDays(30),
], $overrides));
}
function signedDownloadUrl(ReportJob $job, ?Carbon $expiry = null): string
{
return URL::temporarySignedRoute(
'reports.download',
$expiry ?? Carbon::now()->addHours(24),
['id' => $job->id, 'tenant' => $job->tenant_id],
);
}
test('download: success → 200 + attachment файла', function () {
$path = "reports/{$this->tenant->id}/1.csv";
Storage::disk('local')->put($path, "col\r\nval\r\n");
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => $path]);
$response = $this->get(signedDownloadUrl($job));
$response->assertOk();
$response->assertDownload("report-{$job->id}.csv");
});
test('download: невалидная подпись → 403', function () {
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => 'reports/x/1.csv']);
$this->get("/api/reports/jobs/{$job->id}/file?tenant={$this->tenant->id}&expires=9999999999&signature=deadbeef")
->assertStatus(403);
});
test('download: просроченная подпись → 403', function () {
$path = "reports/{$this->tenant->id}/2.csv";
Storage::disk('local')->put($path, 'data');
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => $path]);
$this->get(signedDownloadUrl($job, Carbon::now()->subHour()))->assertStatus(403);
});
test('download: file_path=NULL (истёк) → 410', function () {
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => null]);
$this->get(signedDownloadUrl($job))->assertStatus(410);
});
test('download: файл отсутствует на диске → 404', function () {
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => 'reports/missing/9.csv']);
$this->get(signedDownloadUrl($job))->assertStatus(404);
});
test('download: несуществующий job → 404', function () {
$ghost = new ReportJob(['tenant_id' => $this->tenant->id]);
$ghost->id = 999999;
$this->get(signedDownloadUrl($ghost))->assertStatus(404);
});
test('toResource (GET /jobs/{id}): done-job содержит download_url', function () {
$path = "reports/{$this->tenant->id}/3.csv";
Storage::disk('local')->put($path, 'data');
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => $path]);
$response = $this->actingAs($this->user)->getJson("/api/reports/jobs/{$job->id}");
$response->assertOk();
$url = $response->json('job.download_url');
expect($url)->toContain("/api/reports/jobs/{$job->id}/file");
expect($url)->toContain('signature=');
});
test('toResource: pending-job → download_url = null', function () {
$job = ReportJob::create([
'tenant_id' => $this->tenant->id,
'user_id' => $this->user->id,
'type' => 'deals_export',
'parameters' => ['format' => 'csv', 'date_from' => '2026-04-01', 'date_to' => '2026-04-30'],
'status' => ReportJob::STATUS_PENDING,
]);
$response = $this->actingAs($this->user)->getJson("/api/reports/jobs/{$job->id}");
expect($response->json('job.download_url'))->toBeNull();
});
test('download: expires_at в прошлом → 410 (до cron-очистки file_path)', function () {
$path = "reports/{$this->tenant->id}/4.csv";
Storage::disk('local')->put($path, 'data');
$job = doneReportJob($this->tenant->id, $this->user->id, [
'file_path' => $path,
'expires_at' => Carbon::now()->subDay(),
]);
$this->get(signedDownloadUrl($job))->assertStatus(410);
});
test('toResource: expired done-job → download_url = null', function () {
$path = "reports/{$this->tenant->id}/5.csv";
Storage::disk('local')->put($path, 'data');
$job = doneReportJob($this->tenant->id, $this->user->id, [
'file_path' => $path,
'expires_at' => Carbon::now()->subDay(),
]);
$response = $this->actingAs($this->user)->getJson("/api/reports/jobs/{$job->id}");
expect($response->json('job.download_url'))->toBeNull();
});
test('download: подмена tenant в signed URL ломает подпись → 403', function () {
$path = "reports/{$this->tenant->id}/6.csv";
Storage::disk('local')->put($path, 'data');
$job = doneReportJob($this->tenant->id, $this->user->id, ['file_path' => $path]);
$url = signedDownloadUrl($job);
$otherTenant = Tenant::factory()->create();
// Подменяем tenant=<own> на tenant=<other> в готовом signed URL — подпись
// покрывает query-параметры, поэтому ValidateSignature вернёт 403.
$tampered = str_replace(
"tenant={$this->tenant->id}",
"tenant={$otherTenant->id}",
$url,
);
$this->get($tampered)->assertStatus(403);
});