SparePartControllerTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Models\Role;
  4. use App\Models\SparePart;
  5. use App\Models\User;
  6. use Illuminate\Foundation\Testing\RefreshDatabase;
  7. use Illuminate\Support\Facades\Queue;
  8. use Tests\TestCase;
  9. class SparePartControllerTest extends TestCase
  10. {
  11. use RefreshDatabase;
  12. protected $seed = true;
  13. private User $adminUser;
  14. private User $managerUser;
  15. protected function setUp(): void
  16. {
  17. parent::setUp();
  18. $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
  19. $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
  20. }
  21. // ==================== Authentication ====================
  22. public function test_guest_cannot_access_spare_parts_index(): void
  23. {
  24. $response = $this->get(route('spare_parts.index'));
  25. $response->assertRedirect(route('login'));
  26. }
  27. public function test_admin_can_access_spare_parts_index(): void
  28. {
  29. $response = $this->actingAs($this->adminUser)
  30. ->get(route('spare_parts.index'));
  31. $response->assertStatus(200);
  32. $response->assertViewIs('spare_parts.index');
  33. }
  34. public function test_manager_can_access_spare_parts_index(): void
  35. {
  36. $response = $this->actingAs($this->managerUser)
  37. ->get(route('spare_parts.index'));
  38. $response->assertStatus(200);
  39. $response->assertViewIs('spare_parts.index');
  40. }
  41. // ==================== Show ====================
  42. public function test_admin_can_view_spare_part(): void
  43. {
  44. $sparePart = SparePart::factory()->create();
  45. $response = $this->actingAs($this->adminUser)
  46. ->get(route('spare_parts.show', $sparePart));
  47. $response->assertStatus(200);
  48. $response->assertViewIs('spare_parts.edit');
  49. }
  50. public function test_manager_can_view_spare_part(): void
  51. {
  52. $sparePart = SparePart::factory()->create();
  53. $response = $this->actingAs($this->managerUser)
  54. ->get(route('spare_parts.show', $sparePart));
  55. $response->assertStatus(200);
  56. $response->assertViewIs('spare_parts.edit');
  57. }
  58. public function test_spare_part_show_uses_nav_context_back_url(): void
  59. {
  60. $sparePart = SparePart::factory()->create();
  61. $indexResponse = $this->actingAs($this->adminUser)
  62. ->get(route('spare_parts.index', [
  63. 'filters' => ['used_in_maf' => 'МАФ-42'],
  64. ]));
  65. $nav = $indexResponse->viewData('nav');
  66. $response = $this->actingAs($this->adminUser)
  67. ->get(route('spare_parts.show', [
  68. 'sparePart' => $sparePart,
  69. 'nav' => $nav,
  70. ]));
  71. $response->assertOk();
  72. $response->assertViewHas('nav', $nav);
  73. $response->assertViewHas('back_url', function (string $backUrl): bool {
  74. if (!str_starts_with($backUrl, route('spare_parts.index'))) {
  75. return false;
  76. }
  77. $query = parse_url($backUrl, PHP_URL_QUERY);
  78. parse_str((string) $query, $params);
  79. return ($params['filters']['used_in_maf'] ?? null) === 'МАФ-42';
  80. });
  81. }
  82. public function test_guest_cannot_view_spare_part(): void
  83. {
  84. $sparePart = SparePart::factory()->create();
  85. $response = $this->get(route('spare_parts.show', $sparePart));
  86. $response->assertRedirect(route('login'));
  87. }
  88. // ==================== Store (create) ====================
  89. public function test_admin_can_create_spare_part(): void
  90. {
  91. $response = $this->actingAs($this->adminUser)
  92. ->post(route('spare_parts.store'), [
  93. 'article' => 'SP-TEST-001',
  94. 'used_in_maf' => 'MAF-100',
  95. 'customer_price' => 150.00,
  96. 'purchase_price' => 100.00,
  97. 'min_stock' => 5,
  98. ]);
  99. $response->assertRedirect();
  100. $this->assertDatabaseHas('spare_parts', [
  101. 'article' => 'SP-TEST-001',
  102. 'used_in_maf' => 'MAF-100',
  103. ]);
  104. }
  105. public function test_store_spare_part_redirects_to_parent_url_from_nav_context(): void
  106. {
  107. $response = $this->actingAs($this->adminUser)
  108. ->withSession([
  109. 'navigation' => [
  110. 'spare-part-nav-token' => [
  111. 'updated_at' => time(),
  112. 'stack' => [
  113. route('spare_parts.index'),
  114. route('spare_parts.create'),
  115. ],
  116. ],
  117. ],
  118. ])
  119. ->post(route('spare_parts.store'), [
  120. 'nav' => 'spare-part-nav-token',
  121. 'article' => 'SP-NAV-001',
  122. 'used_in_maf' => 'MAF-200',
  123. 'customer_price' => 250.00,
  124. ]);
  125. $response->assertRedirect(route('spare_parts.index', ['nav' => 'spare-part-nav-token']));
  126. }
  127. public function test_store_requires_article(): void
  128. {
  129. $response = $this->actingAs($this->adminUser)
  130. ->post(route('spare_parts.store'), [
  131. 'customer_price' => 150.00,
  132. ]);
  133. $response->assertSessionHasErrors('article');
  134. }
  135. public function test_manager_cannot_create_spare_part(): void
  136. {
  137. $response = $this->actingAs($this->managerUser)
  138. ->post(route('spare_parts.store'), [
  139. 'article' => 'SP-TEST-MANAGER',
  140. 'customer_price' => 150.00,
  141. ]);
  142. $response->assertStatus(403);
  143. }
  144. public function test_guest_cannot_create_spare_part(): void
  145. {
  146. $response = $this->post(route('spare_parts.store'), [
  147. 'article' => 'SP-TEST-GUEST',
  148. 'customer_price' => 150.00,
  149. ]);
  150. $response->assertRedirect(route('login'));
  151. }
  152. // ==================== Update ====================
  153. public function test_admin_can_update_spare_part(): void
  154. {
  155. $sparePart = SparePart::factory()->create(['article' => 'SP-OLD', 'min_stock' => 3]);
  156. $response = $this->actingAs($this->adminUser)
  157. ->put(route('spare_parts.update', $sparePart), [
  158. 'article' => 'SP-UPDATED',
  159. 'min_stock' => 10,
  160. ]);
  161. $response->assertRedirect();
  162. $this->assertDatabaseHas('spare_parts', [
  163. 'id' => $sparePart->id,
  164. 'article' => 'SP-UPDATED',
  165. 'min_stock' => 10,
  166. ]);
  167. }
  168. public function test_update_spare_part_redirects_to_parent_url_from_nav_context(): void
  169. {
  170. $sparePart = SparePart::factory()->create(['article' => 'SP-OLD']);
  171. $response = $this->actingAs($this->adminUser)
  172. ->withSession([
  173. 'navigation' => [
  174. 'spare-part-update-token' => [
  175. 'updated_at' => time(),
  176. 'stack' => [
  177. route('spare_parts.index'),
  178. route('spare_parts.show', $sparePart),
  179. ],
  180. ],
  181. ],
  182. ])
  183. ->put(route('spare_parts.update', $sparePart), [
  184. 'nav' => 'spare-part-update-token',
  185. 'article' => 'SP-UPDATED-NAV',
  186. 'min_stock' => 9,
  187. ]);
  188. $response->assertRedirect(route('spare_parts.index', ['nav' => 'spare-part-update-token']));
  189. }
  190. public function test_manager_cannot_update_spare_part(): void
  191. {
  192. $sparePart = SparePart::factory()->create();
  193. $response = $this->actingAs($this->managerUser)
  194. ->put(route('spare_parts.update', $sparePart), [
  195. 'article' => 'SP-MANAGER-UPDATE',
  196. ]);
  197. $response->assertStatus(403);
  198. }
  199. public function test_guest_cannot_update_spare_part(): void
  200. {
  201. $sparePart = SparePart::factory()->create();
  202. $response = $this->put(route('spare_parts.update', $sparePart), [
  203. 'article' => 'SP-GUEST-UPDATE',
  204. ]);
  205. $response->assertRedirect(route('login'));
  206. }
  207. // ==================== Destroy ====================
  208. public function test_admin_can_delete_spare_part(): void
  209. {
  210. $sparePart = SparePart::factory()->create();
  211. $sparePartId = $sparePart->id;
  212. $response = $this->actingAs($this->adminUser)
  213. ->delete(route('spare_parts.destroy', $sparePart));
  214. $response->assertRedirect(route('spare_parts.index'));
  215. $this->assertSoftDeleted('spare_parts', ['id' => $sparePartId]);
  216. }
  217. public function test_cannot_delete_spare_part_with_orders(): void
  218. {
  219. $sparePart = SparePart::factory()->create();
  220. // Создаём заказ запчасти через фабрику чтобы привязать к запчасти
  221. \App\Models\SparePartOrder::factory()->create(['spare_part_id' => $sparePart->id]);
  222. $response = $this->actingAs($this->adminUser)
  223. ->delete(route('spare_parts.destroy', $sparePart));
  224. $response->assertRedirect();
  225. $response->assertSessionHas('error');
  226. $this->assertDatabaseHas('spare_parts', ['id' => $sparePart->id, 'deleted_at' => null]);
  227. }
  228. public function test_manager_cannot_delete_spare_part(): void
  229. {
  230. $sparePart = SparePart::factory()->create();
  231. $response = $this->actingAs($this->managerUser)
  232. ->delete(route('spare_parts.destroy', $sparePart));
  233. $response->assertStatus(403);
  234. }
  235. public function test_guest_cannot_delete_spare_part(): void
  236. {
  237. $sparePart = SparePart::factory()->create();
  238. $response = $this->delete(route('spare_parts.destroy', $sparePart));
  239. $response->assertRedirect(route('login'));
  240. }
  241. // ==================== Export ====================
  242. public function test_admin_can_trigger_export(): void
  243. {
  244. Queue::fake();
  245. $response = $this->actingAs($this->adminUser)
  246. ->post(route('spare_parts.export'));
  247. $response->assertRedirect();
  248. $response->assertSessionHas('success');
  249. Queue::assertPushed(\App\Jobs\Export\ExportSparePartsJob::class);
  250. }
  251. public function test_manager_cannot_trigger_export(): void
  252. {
  253. $response = $this->actingAs($this->managerUser)
  254. ->post(route('spare_parts.export'));
  255. $response->assertStatus(403);
  256. }
  257. public function test_guest_cannot_trigger_export(): void
  258. {
  259. $response = $this->post(route('spare_parts.export'));
  260. $response->assertRedirect(route('login'));
  261. }
  262. // ==================== Search API ====================
  263. public function test_admin_can_search_spare_parts(): void
  264. {
  265. SparePart::factory()->create([
  266. 'article' => 'SP-SEARCH-001',
  267. 'note' => 'Комментарий поиска',
  268. ]);
  269. $response = $this->actingAs($this->adminUser)
  270. ->getJson(route('spare_parts.search', ['query' => 'SP-SEARCH']));
  271. $response->assertStatus(200);
  272. $response->assertJsonStructure([['id', 'article', 'note']]);
  273. $response->assertJsonFragment([
  274. 'article' => 'SP-SEARCH-001',
  275. 'note' => 'Комментарий поиска',
  276. ]);
  277. }
  278. public function test_spare_part_show_displays_ordered_row_separately_from_stock(): void
  279. {
  280. $sparePart = SparePart::factory()->create(['min_stock' => 5]);
  281. \App\Models\SparePartOrder::factory()->forSparePart($sparePart)->ordered()->withDocuments(false)->withQuantity(4)->create();
  282. \App\Models\SparePartOrder::factory()->forSparePart($sparePart)->ordered()->withDocuments(true)->withQuantity(6)->create();
  283. \App\Models\SparePartOrder::factory()->forSparePart($sparePart)->inStock()->withDocuments(false)->withQuantity(10)->create();
  284. $response = $this->actingAs($this->adminUser)
  285. ->get(route('spare_parts.show', $sparePart));
  286. $response->assertOk();
  287. $response->assertSee('Заказано');
  288. $response->assertSee('>4<', false);
  289. $response->assertSee('>6<', false);
  290. $response->assertSee('>10<', false);
  291. }
  292. public function test_spare_part_search_can_find_by_note(): void
  293. {
  294. SparePart::factory()->create([
  295. 'article' => 'SP-NOTE-001',
  296. 'note' => 'Особая отметка',
  297. ]);
  298. $response = $this->actingAs($this->adminUser)
  299. ->getJson(route('spare_parts.search', ['query' => 'Особая отметка']));
  300. $response->assertOk();
  301. $response->assertJsonFragment([
  302. 'article' => 'SP-NOTE-001',
  303. 'note' => 'Особая отметка',
  304. ]);
  305. }
  306. public function test_guest_cannot_search_spare_parts(): void
  307. {
  308. $response = $this->get(route('spare_parts.search', ['query' => 'SP-SEARCH']));
  309. $response->assertRedirect(route('login'));
  310. }
  311. }