CommonCatalogControllerTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. <?php
  2. declare(strict_types=1);
  3. namespace Tests\Feature;
  4. use App\Jobs\Export\ExportCommonCatalogJob;
  5. use App\Jobs\Import\ImportJob;
  6. use App\Models\CommonCatalogItem;
  7. use App\Models\Role;
  8. use App\Models\User;
  9. use Illuminate\Foundation\Testing\RefreshDatabase;
  10. use Illuminate\Http\UploadedFile;
  11. use Illuminate\Support\Facades\Bus;
  12. use Illuminate\Support\Facades\Storage;
  13. use Tests\TestCase;
  14. class CommonCatalogControllerTest extends TestCase
  15. {
  16. use RefreshDatabase;
  17. protected bool $seed = true;
  18. private User $admin;
  19. private User $manager;
  20. private User $assistantHead;
  21. protected function setUp(): void
  22. {
  23. parent::setUp();
  24. $this->admin = User::factory()->create(['role' => Role::ADMIN]);
  25. $this->manager = User::factory()->create(['role' => Role::MANAGER]);
  26. $this->assistantHead = User::factory()->create(['role' => Role::ASSISTANT_HEAD]);
  27. }
  28. public function test_guest_cannot_access_common_catalog(): void
  29. {
  30. $this->get(route('common-catalog.index'))->assertRedirect(route('login'));
  31. }
  32. public function test_standard_authenticated_roles_can_view_year_independent_catalog(): void
  33. {
  34. $item = CommonCatalogItem::factory()->create([
  35. 'article' => '0254',
  36. 'calculator_name' => 'Оборудование для благоустройства',
  37. ]);
  38. $this->actingAs($this->manager)
  39. ->withSession(['year' => 2021])
  40. ->get(route('common-catalog.index'))
  41. ->assertOk()
  42. ->assertViewIs('common_catalog.index')
  43. ->assertSee($item->article)
  44. ->assertSee($item->calculator_name);
  45. }
  46. public function test_manager_cannot_mutate_common_catalog(): void
  47. {
  48. $this->actingAs($this->manager)
  49. ->post(route('common-catalog.store'), $this->validData())
  50. ->assertForbidden();
  51. }
  52. public function test_prices_are_visible_to_admin_and_hidden_from_other_roles_by_default(): void
  53. {
  54. $item = CommonCatalogItem::factory()->create([
  55. 'article' => 'PRICE-001',
  56. 'builders_price' => 123456.78,
  57. ]);
  58. $this->actingAs($this->admin)
  59. ->get(route('common-catalog.show', $item))
  60. ->assertOk()
  61. ->assertSeeText('Цены')
  62. ->assertSeeText('строители')
  63. ->assertSee('123456.78', false);
  64. $this->actingAs($this->manager)
  65. ->get(route('common-catalog.show', $item))
  66. ->assertOk()
  67. ->assertDontSeeText('Цены')
  68. ->assertDontSeeText('строители')
  69. ->assertDontSee('123456.78', false);
  70. $this->actingAs($this->assistantHead)
  71. ->get(route('common-catalog.show', $item))
  72. ->assertOk()
  73. ->assertDontSeeText('Цены')
  74. ->assertDontSeeText('строители');
  75. }
  76. public function test_assistant_cannot_change_hidden_price_with_forged_payload(): void
  77. {
  78. $item = CommonCatalogItem::factory()->create([
  79. 'calculator_name' => 'Старое наименование',
  80. 'builders_price' => 100,
  81. ]);
  82. $payload = $this->validData();
  83. $payload['article'] = $item->article;
  84. $payload['calculator_name'] = 'Новое наименование';
  85. $payload['builders_price'] = 999999;
  86. $this->actingAs($this->assistantHead)
  87. ->put(route('common-catalog.update', $item), $payload)
  88. ->assertRedirect();
  89. $item->refresh();
  90. $this->assertSame('Новое наименование', $item->calculator_name);
  91. $this->assertSame(100.0, $item->builders_price);
  92. }
  93. public function test_export_is_available_only_to_admin_by_default(): void
  94. {
  95. Bus::fake([ExportCommonCatalogJob::class]);
  96. $this->actingAs($this->assistantHead)
  97. ->post(route('common-catalog.export'))
  98. ->assertForbidden();
  99. Bus::assertNotDispatched(ExportCommonCatalogJob::class);
  100. $this->actingAs($this->admin)
  101. ->post(route('common-catalog.export'))
  102. ->assertRedirect(route('common-catalog.index'));
  103. Bus::assertDispatched(ExportCommonCatalogJob::class);
  104. }
  105. public function test_admin_can_create_update_and_delete_item(): void
  106. {
  107. $this->actingAs($this->admin)
  108. ->post(route('common-catalog.store'), $this->validData())
  109. ->assertRedirect();
  110. $item = CommonCatalogItem::query()->where('article', '0007')->firstOrFail();
  111. $updated = $this->validData();
  112. $updated['calculator_name'] = 'Обновлённое наименование';
  113. $this->actingAs($this->admin)
  114. ->put(route('common-catalog.update', $item), $updated)
  115. ->assertRedirect();
  116. $this->assertDatabaseHas('common_catalog_items', [
  117. 'id' => $item->id,
  118. 'article' => '0007',
  119. 'calculator_name' => 'Обновлённое наименование',
  120. ]);
  121. $this->actingAs($this->admin)
  122. ->delete(route('common-catalog.destroy', $item))
  123. ->assertRedirect(route('common-catalog.index'));
  124. $this->assertSoftDeleted('common_catalog_items', ['id' => $item->id]);
  125. }
  126. public function test_article_and_name_are_required_and_their_pair_is_unique(): void
  127. {
  128. CommonCatalogItem::factory()->create([
  129. 'article' => '0007',
  130. 'calculator_name' => 'Игровой комплекс',
  131. ]);
  132. $this->actingAs($this->admin)
  133. ->post(route('common-catalog.store'), [
  134. 'article' => '',
  135. 'calculator_name' => '',
  136. ])
  137. ->assertSessionHasErrors(['article', 'calculator_name']);
  138. $this->actingAs($this->admin)
  139. ->post(route('common-catalog.store'), [
  140. 'article' => '0007',
  141. 'calculator_name' => 'Игровой комплекс',
  142. ])
  143. ->assertSessionHasErrors(['article']);
  144. }
  145. public function test_same_article_is_allowed_for_different_named_variants(): void
  146. {
  147. CommonCatalogItem::factory()->create([
  148. 'article' => '6416',
  149. 'calculator_name' => 'Оборудование игровой площадки',
  150. ]);
  151. $payload = $this->validData();
  152. $payload['article'] = '6416';
  153. $payload['calculator_name'] = 'Оборудование игровой площадки (Цинк)';
  154. $this->actingAs($this->admin)
  155. ->post(route('common-catalog.store'), $payload)
  156. ->assertRedirect();
  157. $this->assertDatabaseCount('common_catalog_items', 2);
  158. }
  159. public function test_item_card_contains_technical_description_and_calculation_placeholders(): void
  160. {
  161. $item = CommonCatalogItem::factory()->create();
  162. $this->actingAs($this->admin)
  163. ->get(route('common-catalog.show', $item))
  164. ->assertOk()
  165. ->assertSeeText('Техническое описание')
  166. ->assertSeeText('Калькуляция');
  167. }
  168. public function test_admin_can_upload_and_delete_image(): void
  169. {
  170. Storage::fake('public');
  171. $item = CommonCatalogItem::factory()->create();
  172. $this->actingAs($this->admin)
  173. ->post(route('common-catalog.image.upload', $item), [
  174. 'image' => UploadedFile::fake()->image('item.png', 200, 200),
  175. ])
  176. ->assertRedirect();
  177. $item->refresh();
  178. $this->assertNotNull($item->imageFile);
  179. Storage::disk('public')->assertExists($item->imageFile->path);
  180. $fileId = $item->imageFile->id;
  181. $this->actingAs($this->admin)
  182. ->delete(route('common-catalog.image.delete', $item))
  183. ->assertRedirect();
  184. $this->assertDatabaseMissing('files', ['id' => $fileId]);
  185. $this->assertNull($item->refresh()->image_file_id);
  186. }
  187. public function test_admin_can_upload_and_delete_multiple_documents(): void
  188. {
  189. Storage::fake('public');
  190. $item = CommonCatalogItem::factory()->create();
  191. foreach (['certificate.pdf', 'manual.pdf'] as $filename) {
  192. $this->actingAs($this->admin)
  193. ->post(route('common-catalog.documents.upload', $item), [
  194. 'document' => UploadedFile::fake()->create($filename, 20, 'application/pdf'),
  195. ])
  196. ->assertRedirect();
  197. }
  198. $this->assertCount(2, $item->refresh()->documents);
  199. $document = $item->documents->first();
  200. $this->actingAs($this->admin)
  201. ->delete(route('common-catalog.documents.delete', [$item, $document]))
  202. ->assertRedirect();
  203. $this->assertCount(1, $item->refresh()->documents);
  204. $this->assertDatabaseMissing('files', ['id' => $document->id]);
  205. }
  206. public function test_import_and_export_are_queued(): void
  207. {
  208. Storage::fake('upload');
  209. Bus::fake([ImportJob::class, ExportCommonCatalogJob::class]);
  210. $this->actingAs($this->admin)
  211. ->post(route('common-catalog.import'), [
  212. 'import_file' => UploadedFile::fake()->create(
  213. 'common-catalog.xlsx',
  214. 20,
  215. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  216. ),
  217. ])
  218. ->assertRedirect(route('common-catalog.index'));
  219. $this->assertDatabaseHas('imports', [
  220. 'type' => 'common_catalog',
  221. 'year' => null,
  222. 'user_id' => $this->admin->id,
  223. ]);
  224. Bus::assertDispatched(ImportJob::class);
  225. $this->actingAs($this->admin)
  226. ->post(route('common-catalog.export'))
  227. ->assertRedirect(route('common-catalog.index'));
  228. Bus::assertDispatched(ExportCommonCatalogJob::class);
  229. }
  230. private function validData(): array
  231. {
  232. return [
  233. 'article' => '0007',
  234. 'calculator_name' => "0007\nИгровой комплекс",
  235. 'kind' => 'Игровое оборудование',
  236. 'dimension_length' => '2',
  237. 'dimension_width' => '3',
  238. 'dimension_height' => '1,5',
  239. 'site_length' => '5',
  240. 'site_width' => '6',
  241. 'fall_height' => 1.2,
  242. 'dimension_unit' => 'м',
  243. 'weight' => 350.5,
  244. 'volume' => 4.25,
  245. 'places' => 3,
  246. 'age_group' => '3+',
  247. 'max_users' => 8,
  248. 'unit' => 'шт.',
  249. 'series' => 'Двор',
  250. 'trademark' => 'Наш Двор',
  251. 'calculator_enabled' => true,
  252. 'builders_price' => 100000,
  253. 'wholesale_price' => 105000,
  254. 'recommended_price' => 110000,
  255. 'retail_price' => 150000,
  256. 'project_price' => 200000,
  257. 'project_with_installation_price' => 240000,
  258. 'pik_price' => 110000,
  259. 'recommended_plus_10_price' => 121000,
  260. ];
  261. }
  262. }