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('POST /api/deals/restore 422 без обязательных полей', function () { $this->postJson('/api/deals/restore', [])->assertStatus(422); }); test('POST /api/deals/restore 401 без auth', function () { auth()->logout(); $this->postJson('/api/deals/restore', ['ids' => [1]])->assertStatus(401); }); test('POST /api/deals/restore восстанавливает soft-deleted + пишет deal.restored', function () { $deal = Deal::factory()->for($this->tenant)->for($this->project)->create(); // Удалим сначала $this->deleteJson('/api/deals', [ 'ids' => [$deal->id], ])->assertStatus(200); // Восстановим $r = $this->postJson('/api/deals/restore', [ 'ids' => [$deal->id], ]); $r->assertStatus(200)->assertJson([ 'restored' => 1, 'requested' => 1, ]); DB::statement('SET app.current_tenant_id = '.$this->tenant->id); expect(DB::table('deals')->where('id', $deal->id)->value('deleted_at'))->toBeNull(); $log = ActivityLog::where('deal_id', $deal->id) ->where('event', 'deal.restored') ->first(); expect($log)->not->toBeNull(); expect($log->context)->toMatchArray(['source' => 'bulk']); }); test('POST /api/deals/restore NO-OP для не-удалённых (живых) сделок', function () { $alive = Deal::factory()->for($this->tenant)->for($this->project)->create(); $r = $this->postJson('/api/deals/restore', [ 'ids' => [$alive->id], ]); $r->assertStatus(200)->assertJson([ 'restored' => 0, 'requested' => 1, ]); // Не пишем audit для NO-OP. DB::statement('SET app.current_tenant_id = '.$this->tenant->id); expect(ActivityLog::where('deal_id', $alive->id)->where('event', 'deal.restored')->count())->toBe(0); }); test('POST /api/deals/restore defense-in-depth не восстанавливает чужие сделки', function () { 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(); $foreign->delete(); // soft-delete DB::statement('SET app.current_tenant_id = '.$this->tenant->id); $own = Deal::factory()->for($this->tenant)->for($this->project)->create(); $own->delete(); $r = $this->postJson('/api/deals/restore', [ 'ids' => [$own->id, $foreign->id], ]); $r->assertStatus(200)->assertJson([ 'restored' => 1, 'requested' => 2, ]); // Свой жив (deleted_at=NULL). DB::statement('SET app.current_tenant_id = '.$this->tenant->id); expect(DB::table('deals')->where('id', $own->id)->value('deleted_at'))->toBeNull(); // Чужой остался удалённым. DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id); expect(DB::table('deals')->where('id', $foreign->id)->value('deleted_at'))->not->toBeNull(); }); test('POST /api/deals/restore — после restore сделка снова видна в GET /api/deals', function () { $deal = Deal::factory()->for($this->tenant)->for($this->project)->create(); // Удалили $this->deleteJson('/api/deals', [ 'ids' => [$deal->id], ])->assertStatus(200); // GET не возвращает expect($this->getJson('/api/deals')->json('total'))->toBe(0); // Restore $this->postJson('/api/deals/restore', [ 'ids' => [$deal->id], ])->assertStatus(200); // GET снова возвращает expect($this->getJson('/api/deals')->json('total'))->toBe(1); }); test('POST /api/deals/restore 422 пустой массив ids', function () { $this->postJson('/api/deals/restore', [ 'ids' => [], ])->assertStatus(422); });