Files
portal/app/tests/Feature/DealRestoreTest.php
T
2026-05-16 15:18:13 +03:00

130 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ActivityLog;
use App\Models\Deal;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->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);
});