tenant = Tenant::factory()->create(); $this->otherTenant = Tenant::factory()->create(); $this->user = User::factory()->for($this->tenant)->create(); $this->actingAs($this->user); DB::statement('SET app.current_tenant_id = '.$this->tenant->id); $this->project = Project::factory()->for($this->tenant)->create(); }); test('DELETE /api/deals 422 без обязательных полей', function () { $this->deleteJson('/api/deals', [])->assertStatus(422); }); test('DELETE /api/deals 401 без auth', function () { auth()->logout(); $this->deleteJson('/api/deals', ['ids' => [1]])->assertStatus(401); }); test('DELETE /api/deals soft-удаляет сделки + пишет deal.deleted ActivityLog', function () { $deals = Deal::factory()->count(3)->for($this->tenant)->for($this->project)->create(); $r = $this->deleteJson('/api/deals', [ 'ids' => $deals->pluck('id')->all(), ]); $r->assertStatus(200)->assertJson([ 'deleted' => 3, 'requested' => 3, ]); DB::statement('SET app.current_tenant_id = '.$this->tenant->id); foreach ($deals as $d) { $row = DB::table('deals')->where('id', $d->id)->first(); expect($row->deleted_at)->not->toBeNull(); } $logs = ActivityLog::where('tenant_id', $this->tenant->id) ->where('event', 'deal.deleted') ->get(); expect($logs)->toHaveCount(3); expect($logs->first()->context)->toMatchArray(['source' => 'bulk']); }); test('DELETE /api/deals defense-in-depth не удаляет чужие сделки', function () { $own = Deal::factory()->for($this->tenant)->for($this->project)->create(); DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id); $foreignProject = Project::factory()->for($this->otherTenant)->create(); $foreign = Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(); $r = $this->deleteJson('/api/deals', [ 'ids' => [$own->id, $foreign->id], ]); $r->assertStatus(200)->assertJson([ 'deleted' => 1, 'requested' => 2, ]); // Свой удалён. DB::statement('SET app.current_tenant_id = '.$this->tenant->id); expect(DB::table('deals')->where('id', $own->id)->value('deleted_at'))->not->toBeNull(); // Чужой жив. DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id); expect(DB::table('deals')->where('id', $foreign->id)->value('deleted_at'))->toBeNull(); }); test('DELETE /api/deals NO-OP на уже удалённых', function () { $deal = Deal::factory()->for($this->tenant)->for($this->project)->create(); // Первое удаление $this->deleteJson('/api/deals', [ 'ids' => [$deal->id], ])->assertStatus(200)->assertJson(['deleted' => 1]); // Повтор — уже удалена, NO-OP. $r = $this->deleteJson('/api/deals', [ 'ids' => [$deal->id], ]); $r->assertStatus(200)->assertJson(['deleted' => 0, 'requested' => 1]); // ActivityLog — только 1 запись (после первого удаления). DB::statement('SET app.current_tenant_id = '.$this->tenant->id); expect(ActivityLog::where('deal_id', $deal->id)->where('event', 'deal.deleted')->count())->toBe(1); }); test('GET /api/deals НЕ возвращает soft-deleted сделки', function () { $alive = Deal::factory()->for($this->tenant)->for($this->project)->create(); $deleted = Deal::factory()->for($this->tenant)->for($this->project)->create(); // Удаляем одну $this->deleteJson('/api/deals', [ 'ids' => [$deleted->id], ])->assertStatus(200); $r = $this->getJson('/api/deals'); $ids = collect($r->json('deals'))->pluck('id')->all(); expect($ids)->toContain($alive->id); expect($ids)->not->toContain($deleted->id); expect($r->json('total'))->toBe(1); }); test('GET /api/deals/{id} 404 для soft-deleted сделки', function () { $deal = Deal::factory()->for($this->tenant)->for($this->project)->create(); $this->deleteJson('/api/deals', [ 'ids' => [$deal->id], ])->assertStatus(200); $this->getJson('/api/deals/'.$deal->id) ->assertStatus(404); }); test('DELETE /api/deals 422 пустой массив ids', function () { $this->deleteJson('/api/deals', [ 'ids' => [], ])->assertStatus(422); });