2026-07-26 11:49:17 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Models\AdCampaign;
|
|
|
|
|
|
use App\Models\AdCampaignBanner;
|
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
|
use App\Models\User;
|
2026-07-26 21:13:20 +03:00
|
|
|
|
use App\Services\Advertising\BannerSizes;
|
|
|
|
|
|
use App\Services\Advertising\BannerUploadPolicy;
|
2026-07-26 11:49:17 +03:00
|
|
|
|
use Illuminate\Http\UploadedFile;
|
|
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
|
|
|
|
|
|
|
function bannerCampaign(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
$user = User::factory()->create(['tenant_id' => $tenant->id]);
|
|
|
|
|
|
$campaign = AdCampaign::create([
|
|
|
|
|
|
'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return [$tenant, $user, $campaign];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
it('список слотов без загрузок содержит все 15 размеров, все не загружены', function () {
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->getJson("/api/advertising/campaigns/{$campaign->id}/banners");
|
|
|
|
|
|
$res->assertOk();
|
|
|
|
|
|
$res->assertJsonPath('approved_at', null);
|
|
|
|
|
|
$res->assertJsonPath('max_bytes', BannerUploadPolicy::MAX_BYTES);
|
|
|
|
|
|
expect($res->json('formats'))->toBe(BannerUploadPolicy::FORMATS);
|
|
|
|
|
|
|
|
|
|
|
|
$slots = $res->json('slots');
|
|
|
|
|
|
expect($slots)->toHaveCount(15);
|
|
|
|
|
|
foreach ($slots as $slot) {
|
|
|
|
|
|
expect($slot['uploaded'])->toBeFalse();
|
|
|
|
|
|
expect($slot['banner_id'])->toBeNull();
|
|
|
|
|
|
expect($slot['preview_url'])->toBeNull();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('загрузка баннера точного размера создаёт слот', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 728, 90),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$res->assertCreated();
|
|
|
|
|
|
expect($res->json('slot.uploaded'))->toBeTrue();
|
|
|
|
|
|
expect($res->json('slot.width'))->toBe(728);
|
|
|
|
|
|
expect($res->json('slot.height'))->toBe(90);
|
|
|
|
|
|
expect($res->json('slot.included'))->toBeTrue();
|
|
|
|
|
|
|
|
|
|
|
|
$banner = AdCampaignBanner::where('campaign_id', $campaign->id)->where('width', 728)->where('height', 90)->first();
|
|
|
|
|
|
expect($banner)->not->toBeNull();
|
|
|
|
|
|
Storage::disk('local')->assertExists($banner->path);
|
|
|
|
|
|
|
|
|
|
|
|
expect($campaign->fresh()->banners_approved_at)->toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('картинка не того размера — 422 с понятным сообщением', function () {
|
2026-07-26 11:49:17 +03:00
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 800, 100),
|
2026-07-26 11:49:17 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
$res->assertStatus(422);
|
|
|
|
|
|
expect($res->json('message'))->toContain('Нужен ровно 728×90');
|
2026-07-26 11:49:17 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
it('неизвестный размер — 422', function () {
|
2026-07-26 11:49:17 +03:00
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
2026-07-26 21:13:20 +03:00
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 999, 'height' => 999,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 999, 999),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$res->assertStatus(422);
|
|
|
|
|
|
expect($res->json('message'))->toBe('Неизвестный размер баннера.');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('не-картинка отклоняется валидатором', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->create('b.txt', 5, 'text/plain'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$res->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('повторная загрузка того же размера заменяет строку, а не дублирует', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b1.jpg', 728, 90),
|
2026-07-26 11:49:17 +03:00
|
|
|
|
])->assertCreated();
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
$campaign->update(['banners_approved_at' => now()]);
|
2026-07-26 11:49:17 +03:00
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b2.png', 728, 90),
|
|
|
|
|
|
]);
|
|
|
|
|
|
$res->assertCreated();
|
|
|
|
|
|
|
|
|
|
|
|
expect(AdCampaignBanner::where('campaign_id', $campaign->id)->where('width', 728)->where('height', 90)->count())->toBe(1);
|
|
|
|
|
|
expect($campaign->fresh()->banners_approved_at)->toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('переключает included баннера', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$up = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 728, 90),
|
|
|
|
|
|
])->assertCreated();
|
|
|
|
|
|
$bannerId = $up->json('slot.banner_id');
|
|
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->patchJson("/api/advertising/campaigns/{$campaign->id}/banners/{$bannerId}", [
|
|
|
|
|
|
'included' => false,
|
|
|
|
|
|
]);
|
|
|
|
|
|
$res->assertOk();
|
|
|
|
|
|
expect($res->json('slot.included'))->toBeFalse();
|
|
|
|
|
|
|
|
|
|
|
|
$banner = AdCampaignBanner::find($bannerId);
|
|
|
|
|
|
expect($banner->included)->toBeFalse();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('удаляет баннер и сбрасывает утверждение', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$up = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 728, 90),
|
|
|
|
|
|
])->assertCreated();
|
|
|
|
|
|
$bannerId = $up->json('slot.banner_id');
|
|
|
|
|
|
$campaign->update(['banners_approved_at' => now()]);
|
|
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->deleteJson("/api/advertising/campaigns/{$campaign->id}/banners/{$bannerId}");
|
|
|
|
|
|
$res->assertStatus(204);
|
|
|
|
|
|
|
|
|
|
|
|
expect(AdCampaignBanner::find($bannerId))->toBeNull();
|
|
|
|
|
|
expect($campaign->fresh()->banners_approved_at)->toBeNull();
|
2026-07-26 11:49:17 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
it('утверждает при хотя бы одном included баннере', function () {
|
2026-07-26 11:49:17 +03:00
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
2026-07-26 21:13:20 +03:00
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 728, 90),
|
2026-07-26 11:49:17 +03:00
|
|
|
|
])->assertCreated();
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners/approve");
|
|
|
|
|
|
$res->assertOk();
|
2026-07-26 11:49:17 +03:00
|
|
|
|
expect($campaign->fresh()->banners_approved_at)->not->toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
it('не утверждает, если все included=false', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
|
|
|
|
|
|
$up = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 728, 90),
|
|
|
|
|
|
])->assertCreated();
|
|
|
|
|
|
$bannerId = $up->json('slot.banner_id');
|
|
|
|
|
|
$this->actingAs($user)->patchJson("/api/advertising/campaigns/{$campaign->id}/banners/{$bannerId}", ['included' => false])->assertOk();
|
|
|
|
|
|
|
|
|
|
|
|
$res = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners/approve");
|
|
|
|
|
|
$res->assertStatus(422);
|
|
|
|
|
|
expect($res->json('message'))->toBe('Отметьте хотя бы один баннер для показа.');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 11:49:17 +03:00
|
|
|
|
it('нельзя утвердить пустой набор', function () {
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
$this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners/approve")->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-26 21:13:20 +03:00
|
|
|
|
it('превью работает и отдаёт content-type по расширению', function () {
|
2026-07-26 11:49:17 +03:00
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
2026-07-26 21:13:20 +03:00
|
|
|
|
|
|
|
|
|
|
$up = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.png', 728, 90),
|
2026-07-26 11:49:17 +03:00
|
|
|
|
])->assertCreated();
|
2026-07-26 21:13:20 +03:00
|
|
|
|
$bannerId = $up->json('slot.banner_id');
|
|
|
|
|
|
|
|
|
|
|
|
$preview = $this->actingAs($user)->get("/api/advertising/campaigns/{$campaign->id}/banners/{$bannerId}/preview");
|
|
|
|
|
|
$preview->assertOk();
|
|
|
|
|
|
expect($preview->headers->get('content-type'))->toContain('image/png');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('чужой тенант не видит превью баннера (404)', function () {
|
|
|
|
|
|
Storage::fake('local');
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
$up = $this->actingAs($user)->postJson("/api/advertising/campaigns/{$campaign->id}/banners", [
|
|
|
|
|
|
'width' => 728, 'height' => 90,
|
|
|
|
|
|
'file' => UploadedFile::fake()->image('b.jpg', 728, 90),
|
|
|
|
|
|
])->assertCreated();
|
|
|
|
|
|
$bannerId = $up->json('slot.banner_id');
|
2026-07-26 11:49:17 +03:00
|
|
|
|
|
|
|
|
|
|
$otherTenant = Tenant::factory()->create();
|
|
|
|
|
|
$otherUser = User::factory()->create(['tenant_id' => $otherTenant->id]);
|
|
|
|
|
|
$this->actingAs($otherUser)->get("/api/advertising/campaigns/{$campaign->id}/banners/{$bannerId}/preview")->assertNotFound();
|
|
|
|
|
|
});
|
2026-07-26 21:13:20 +03:00
|
|
|
|
|
|
|
|
|
|
it('размеры вне BannerSizes перечислены целиком и по порядку', function () {
|
|
|
|
|
|
[, $user, $campaign] = bannerCampaign();
|
|
|
|
|
|
$res = $this->actingAs($user)->getJson("/api/advertising/campaigns/{$campaign->id}/banners");
|
|
|
|
|
|
$slots = $res->json('slots');
|
|
|
|
|
|
foreach (BannerSizes::all() as $i => [$w, $h]) {
|
|
|
|
|
|
expect($slots[$i]['width'])->toBe($w);
|
|
|
|
|
|
expect($slots[$i]['height'])->toBe($h);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|