withHeaders(['Accept' => 'application/json']); $this->tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); /** Черновик кампании тенанта под приложение медиа. */ function draftForMedia(int $tenantId, int $userId): Campaign { return Campaign::create([ 'tenant_id' => $tenantId, 'status' => Campaign::STATUS_DRAFT, 'ad_text' => 'Приходите к нам', 'ad_link' => 'https://t.me/example', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '1000.00', 'audience_kind' => Campaign::AUDIENCE_BASE, 'planned_count' => 0, 'estimated_cost_rub' => '0.00', 'created_by' => $userId, ]); } it('прикладывает картинку к черновику: файл сохранён, media_path записан', function () { $campaign = draftForMedia($this->tenant->id, $this->user->id); $res = $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => UploadedFile::fake()->image('ad.jpg', 1280, 720), ]); $res->assertOk(); $path = Campaign::find($campaign->id)->media_path; expect($path)->not->toBeNull(); Storage::disk('local')->assertExists($path); expect($res->json('media_path'))->toBe($path); }); it('принимает видео mp4', function () { $campaign = draftForMedia($this->tenant->id, $this->user->id); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => UploadedFile::fake()->create('ad.mp4', 200, 'video/mp4'), ])->assertOk(); expect(Campaign::find($campaign->id)->media_path)->not->toBeNull(); }); it('отклоняет чужой тип файла (например .exe) → 422', function () { $campaign = draftForMedia($this->tenant->id, $this->user->id); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => UploadedFile::fake()->create('virus.exe', 10, 'application/octet-stream'), ])->assertStatus(422)->assertJsonValidationErrors('media'); expect(Campaign::find($campaign->id)->media_path)->toBeNull(); }); it('нельзя приложить медиа к не-черновику (уже запущена) → 422', function () { $campaign = draftForMedia($this->tenant->id, $this->user->id); $campaign->update(['status' => Campaign::STATUS_QUEUED]); // Картинка заведомо годная — значит 422 придёт именно за «не черновик», а не за размер. $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => UploadedFile::fake()->image('ad.jpg', 1280, 720), ])->assertStatus(422); }); it('замена медиа удаляет прежний файл', function () { $campaign = draftForMedia($this->tenant->id, $this->user->id); $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => UploadedFile::fake()->image('first.jpg', 1280, 720), ])->assertOk(); $first = Campaign::find($campaign->id)->media_path; $this->post("/api/telegram/campaigns/{$campaign->id}/media", [ 'media' => UploadedFile::fake()->image('second.jpg', 1280, 720), ])->assertOk(); $second = Campaign::find($campaign->id)->media_path; expect($second)->not->toBe($first); Storage::disk('local')->assertMissing($first); Storage::disk('local')->assertExists($second); }); it('изоляция тенантов: чужой черновик → 404', function () { $tenantB = Tenant::factory()->create(); $foreign = draftForMedia($tenantB->id, $this->user->id); $this->post("/api/telegram/campaigns/{$foreign->id}/media", [ 'media' => UploadedFile::fake()->image('ad.jpg'), ])->assertStatus(404); });