| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- <?php
- declare(strict_types=1);
- namespace Tests\Feature;
- use App\Jobs\Export\ExportCommonCatalogJob;
- use App\Jobs\Export\ExportTechnicalDescriptionsJob;
- use App\Jobs\Import\ImportJob;
- use App\Models\CommonCatalogItem;
- use App\Models\File;
- use App\Models\Role;
- use App\Models\User;
- use Illuminate\Foundation\Testing\RefreshDatabase;
- use Illuminate\Http\UploadedFile;
- use Illuminate\Support\Facades\Bus;
- use Illuminate\Support\Facades\Storage;
- use Tests\TestCase;
- class CommonCatalogControllerTest extends TestCase
- {
- use RefreshDatabase;
- protected bool $seed = true;
- private User $admin;
- private User $manager;
- private User $assistantHead;
- protected function setUp(): void
- {
- parent::setUp();
- $this->admin = User::factory()->create(['role' => Role::ADMIN]);
- $this->manager = User::factory()->create(['role' => Role::MANAGER]);
- $this->assistantHead = User::factory()->create(['role' => Role::ASSISTANT_HEAD]);
- }
- public function test_guest_cannot_access_common_catalog(): void
- {
- $this->get(route('common-catalog.index'))->assertRedirect(route('login'));
- }
- public function test_standard_authenticated_roles_can_view_year_independent_catalog(): void
- {
- $item = CommonCatalogItem::factory()->create([
- 'article' => '0254',
- 'calculator_name' => 'Оборудование для благоустройства',
- ]);
- $this->actingAs($this->manager)
- ->withSession(['year' => 2021])
- ->get(route('common-catalog.index'))
- ->assertOk()
- ->assertViewIs('common_catalog.index')
- ->assertSee($item->article)
- ->assertSee($item->calculator_name);
- }
- public function test_common_catalog_column_filter_values_are_available(): void
- {
- CommonCatalogItem::factory()->create([
- 'article' => 'FILTER-001',
- 'builders_price' => 123456.78,
- ]);
- $deletedItem = CommonCatalogItem::factory()->create(['article' => 'FILTER-DELETED']);
- $deletedItem->delete();
- $this->actingAs($this->admin)
- ->getJson(route('getFilters', [
- 'table' => 'common_catalog_items',
- 'column' => 'article',
- ]))
- ->assertOk()
- ->assertJsonFragment(['FILTER-001'])
- ->assertJsonMissing(['FILTER-DELETED']);
- $this->actingAs($this->admin)
- ->getJson(route('getFilters', [
- 'table' => 'common_catalog_items',
- 'column' => 'builders_price_txt',
- ]))
- ->assertOk()
- ->assertJsonFragment([123456.78]);
- }
- public function test_common_catalog_column_filter_is_applied(): void
- {
- CommonCatalogItem::factory()->create([
- 'article' => 'FILTER-MATCH',
- 'calculator_name' => 'Нужная позиция',
- 'builders_price' => 123456.78,
- ]);
- CommonCatalogItem::factory()->create([
- 'article' => 'FILTER-OTHER',
- 'calculator_name' => 'Другая позиция',
- 'builders_price' => 987654.32,
- ]);
- $this->actingAs($this->admin)
- ->get(route('common-catalog.index', [
- 'filters' => ['article' => 'FILTER-MATCH'],
- ]))
- ->assertOk()
- ->assertSee('Нужная позиция')
- ->assertDontSee('Другая позиция');
- $this->actingAs($this->admin)
- ->get(route('common-catalog.index', [
- 'filters' => ['builders_price_txt' => '123456.78'],
- ]))
- ->assertOk()
- ->assertSee('Нужная позиция')
- ->assertDontSee('Другая позиция');
- }
- public function test_common_catalog_filter_values_respect_field_access(): void
- {
- CommonCatalogItem::factory()->create(['builders_price' => 123456.78]);
- $this->actingAs($this->manager)
- ->getJson(route('getFilters', [
- 'table' => 'common_catalog_items',
- 'column' => 'builders_price_txt',
- ]))
- ->assertForbidden();
- }
- public function test_manager_cannot_mutate_common_catalog(): void
- {
- $this->actingAs($this->manager)
- ->post(route('common-catalog.store'), $this->validData())
- ->assertForbidden();
- }
- public function test_prices_are_visible_to_admin_and_hidden_from_other_roles_by_default(): void
- {
- $item = CommonCatalogItem::factory()->create([
- 'article' => 'PRICE-001',
- 'builders_price' => 123456.78,
- ]);
- $this->actingAs($this->admin)
- ->get(route('common-catalog.show', $item))
- ->assertOk()
- ->assertSeeText('Цены')
- ->assertSeeText('строители')
- ->assertSee('123456.78', false);
- $this->actingAs($this->manager)
- ->get(route('common-catalog.show', $item))
- ->assertOk()
- ->assertDontSeeText('Цены')
- ->assertDontSeeText('строители')
- ->assertDontSee('123456.78', false);
- $this->actingAs($this->assistantHead)
- ->get(route('common-catalog.show', $item))
- ->assertOk()
- ->assertDontSeeText('Цены')
- ->assertDontSeeText('строители');
- }
- public function test_assistant_cannot_change_hidden_price_with_forged_payload(): void
- {
- $item = CommonCatalogItem::factory()->create([
- 'calculator_name' => 'Старое наименование',
- 'builders_price' => 100,
- ]);
- $payload = $this->validData();
- $payload['article'] = $item->article;
- $payload['calculator_name'] = 'Новое наименование';
- $payload['builders_price'] = 999999;
- $this->actingAs($this->assistantHead)
- ->put(route('common-catalog.update', $item), $payload)
- ->assertRedirect();
- $item->refresh();
- $this->assertSame('Новое наименование', $item->calculator_name);
- $this->assertSame(100.0, $item->builders_price);
- }
- public function test_export_is_available_only_to_admin_by_default(): void
- {
- Bus::fake([ExportCommonCatalogJob::class]);
- $this->actingAs($this->assistantHead)
- ->post(route('common-catalog.export'))
- ->assertForbidden();
- Bus::assertNotDispatched(ExportCommonCatalogJob::class);
- $this->actingAs($this->admin)
- ->post(route('common-catalog.export'))
- ->assertRedirect(route('common-catalog.index'));
- Bus::assertDispatched(ExportCommonCatalogJob::class);
- }
- public function test_admin_can_create_update_and_delete_item(): void
- {
- $this->actingAs($this->admin)
- ->post(route('common-catalog.store'), $this->validData())
- ->assertRedirect();
- $item = CommonCatalogItem::query()->where('article', '0007')->firstOrFail();
- $updated = $this->validData();
- $updated['calculator_name'] = 'Обновлённое наименование';
- $this->actingAs($this->admin)
- ->put(route('common-catalog.update', $item), $updated)
- ->assertRedirect();
- $this->assertDatabaseHas('common_catalog_items', [
- 'id' => $item->id,
- 'article' => '0007',
- 'calculator_name' => 'Обновлённое наименование',
- ]);
- $this->actingAs($this->admin)
- ->delete(route('common-catalog.destroy', $item))
- ->assertRedirect(route('common-catalog.index'));
- $this->assertSoftDeleted('common_catalog_items', ['id' => $item->id]);
- }
- public function test_article_and_name_are_required_and_their_pair_is_unique(): void
- {
- CommonCatalogItem::factory()->create([
- 'article' => '0007',
- 'calculator_name' => 'Игровой комплекс',
- ]);
- $this->actingAs($this->admin)
- ->post(route('common-catalog.store'), [
- 'article' => '',
- 'calculator_name' => '',
- ])
- ->assertSessionHasErrors(['article', 'calculator_name']);
- $this->actingAs($this->admin)
- ->post(route('common-catalog.store'), [
- 'article' => '0007',
- 'calculator_name' => 'Игровой комплекс',
- ])
- ->assertSessionHasErrors(['article']);
- }
- public function test_same_article_is_allowed_for_different_named_variants(): void
- {
- CommonCatalogItem::factory()->create([
- 'article' => '6416',
- 'calculator_name' => 'Оборудование игровой площадки',
- ]);
- $payload = $this->validData();
- $payload['article'] = '6416';
- $payload['calculator_name'] = 'Оборудование игровой площадки (Цинк)';
- $this->actingAs($this->admin)
- ->post(route('common-catalog.store'), $payload)
- ->assertRedirect();
- $this->assertDatabaseCount('common_catalog_items', 2);
- }
- public function test_item_card_contains_technical_description_and_calculation_placeholders(): void
- {
- $item = CommonCatalogItem::factory()->create();
- $this->actingAs($this->admin)
- ->get(route('common-catalog.show', $item))
- ->assertOk()
- ->assertSeeText('Техническое описание')
- ->assertSeeText('Калькуляция');
- }
- public function test_admin_can_edit_technical_description_fields(): void
- {
- $item = CommonCatalogItem::factory()->create();
- $payload = $this->validData();
- $payload['article'] = $item->article;
- $payload['print_name'] = 'Наименование для формы';
- $payload['product_group'] = 'Игровое оборудование';
- $payload['characteristics'] = 'Характеристики';
- $payload['technical_description'] = 'Полное описание';
- $payload['technical_description_short'] = 'Краткое описание';
- $this->actingAs($this->admin)
- ->put(route('common-catalog.update', $item), $payload)
- ->assertRedirect();
- $this->assertDatabaseHas('common_catalog_items', [
- 'id' => $item->id,
- 'print_name' => 'Наименование для формы',
- 'technical_description' => 'Полное описание',
- ]);
- }
- public function test_technical_description_export_requires_access_to_selected_price(): void
- {
- Bus::fake([ExportTechnicalDescriptionsJob::class]);
- $item = CommonCatalogItem::factory()->create();
- $payload = [
- 'item_ids' => [$item->id],
- 'description_mode' => \App\Services\TechnicalDescriptionService::MODE_CHARACTERISTICS_SHORT,
- ];
- $this->actingAs($this->manager)
- ->post(route('common-catalog.technical-description.export'), $payload)
- ->assertForbidden();
- Bus::assertNotDispatched(ExportTechnicalDescriptionsJob::class);
- $this->actingAs($this->admin)
- ->post(route('common-catalog.technical-description.export'), $payload)
- ->assertRedirect();
- Bus::assertDispatched(ExportTechnicalDescriptionsJob::class);
- }
- public function test_generated_technical_description_is_private_to_owner_or_admin(): void
- {
- Storage::fake('local');
- $owner = User::factory()->create(['role' => Role::MANAGER]);
- $otherManager = User::factory()->create(['role' => Role::MANAGER]);
- $path = 'generated/technical-descriptions/'.$owner->id.'/test/document.docx';
- Storage::disk('local')->put($path, 'docx');
- $file = File::query()->create([
- 'user_id' => $owner->id,
- 'path' => $path,
- 'link' => '',
- 'original_name' => 'document.docx',
- 'mime_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'is_generated' => true,
- ]);
- $this->actingAs($otherManager)
- ->get(route('common-catalog.technical-description.download', $file))
- ->assertForbidden();
- $this->actingAs($owner)
- ->get(route('common-catalog.technical-description.download', $file))
- ->assertDownload('document.docx');
- $this->actingAs($this->admin)
- ->get(route('common-catalog.technical-description.download', $file))
- ->assertDownload('document.docx');
- }
- public function test_admin_can_upload_and_delete_image(): void
- {
- Storage::fake('public');
- $item = CommonCatalogItem::factory()->create();
- $this->actingAs($this->admin)
- ->post(route('common-catalog.image.upload', $item), [
- 'image' => UploadedFile::fake()->image('item.png', 200, 200),
- ])
- ->assertRedirect();
- $item->refresh();
- $this->assertNotNull($item->imageFile);
- Storage::disk('public')->assertExists($item->imageFile->path);
- $fileId = $item->imageFile->id;
- $this->actingAs($this->admin)
- ->delete(route('common-catalog.image.delete', $item))
- ->assertRedirect();
- $this->assertDatabaseMissing('files', ['id' => $fileId]);
- $this->assertNull($item->refresh()->image_file_id);
- }
- public function test_admin_can_upload_and_delete_multiple_documents(): void
- {
- Storage::fake('public');
- $item = CommonCatalogItem::factory()->create();
- foreach (['certificate.pdf', 'manual.pdf'] as $filename) {
- $this->actingAs($this->admin)
- ->post(route('common-catalog.documents.upload', $item), [
- 'document' => UploadedFile::fake()->create($filename, 20, 'application/pdf'),
- ])
- ->assertRedirect();
- }
- $this->assertCount(2, $item->refresh()->documents);
- $document = $item->documents->first();
- $this->actingAs($this->admin)
- ->delete(route('common-catalog.documents.delete', [$item, $document]))
- ->assertRedirect();
- $this->assertCount(1, $item->refresh()->documents);
- $this->assertDatabaseMissing('files', ['id' => $document->id]);
- }
- public function test_import_and_export_are_queued(): void
- {
- Storage::fake('upload');
- Bus::fake([ImportJob::class, ExportCommonCatalogJob::class]);
- $this->actingAs($this->admin)
- ->post(route('common-catalog.import'), [
- 'import_file' => UploadedFile::fake()->create(
- 'common-catalog.xlsx',
- 20,
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- ),
- ])
- ->assertRedirect(route('common-catalog.index'));
- $this->assertDatabaseHas('imports', [
- 'type' => 'common_catalog',
- 'year' => null,
- 'user_id' => $this->admin->id,
- ]);
- Bus::assertDispatched(ImportJob::class);
- $this->actingAs($this->admin)
- ->post(route('common-catalog.export'))
- ->assertRedirect(route('common-catalog.index'));
- Bus::assertDispatched(ExportCommonCatalogJob::class);
- }
- private function validData(): array
- {
- return [
- 'article' => '0007',
- 'calculator_name' => "0007\nИгровой комплекс",
- 'kind' => 'Игровое оборудование',
- 'dimension_length' => '2',
- 'dimension_width' => '3',
- 'dimension_height' => '1,5',
- 'site_length' => '5',
- 'site_width' => '6',
- 'fall_height' => 1.2,
- 'dimension_unit' => 'м',
- 'weight' => 350.5,
- 'volume' => 4.25,
- 'places' => 3,
- 'age_group' => '3+',
- 'max_users' => 8,
- 'unit' => 'шт.',
- 'series' => 'Двор',
- 'trademark' => 'Наш Двор',
- 'calculator_enabled' => true,
- 'builders_price' => 100000,
- 'wholesale_price' => 105000,
- 'recommended_price' => 110000,
- 'retail_price' => 150000,
- 'project_price' => 200000,
- 'project_with_installation_price' => 240000,
- 'pik_price' => 110000,
- 'recommended_plus_10_price' => 121000,
- ];
- }
- }
|