Files
Investidor-V2/tests/Feature/PartnershipDraftRecoveryTest.php

185 lines
10 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\HandleInertiaRequests;
use App\Models\Sector;
use App\Models\User;
use App\Services\PartnershipExchange;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
final class PartnershipDraftRecoveryTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private object $record;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Http::fake();
Mail::fake();
Queue::fake();
$this->owner = User::factory()->create();
$this->actingAs($this->owner);
$this->record = app(PartnershipExchange::class)->save($this->owner, ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC original draft', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC offer', 'payload' => ['consent' => true, 'mandatory' => ['capacity']]]);
}
private function command(array $changes = []): array
{
return ['operation_key' => (string) Str::uuid(), 'version' => $this->record->version, 'kind' => 'seeking', 'title' => 'SYNTHETIC saved revision', 'partnership_type' => 'joint_venture', 'payload' => ['mandatory' => ['capacity']], ...$changes];
}
private function url(): string
{
return '/parcerias/'.$this->record->id;
}
public function test_repeating_a_committed_edit_preserves_a_later_state_and_does_not_create_another_revision(): void
{
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
$current = app(PartnershipExchange::class)->find($this->record->id);
app(PartnershipExchange::class)->transition($this->owner, $current->id, $current->version, 'submit', 'SYNTHETIC submit after edit');
$events = DB::table('partnership_events')->count();
$audits = DB::table('audit_events')->count();
$notices = DB::table('partnership_notifications')->count();
$this->putJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('saved_version', $data['version'] + 1)->assertJsonPath('status', 'submitted');
$this->assertSame($events, DB::table('partnership_events')->count());
$this->assertSame($audits, DB::table('audit_events')->count());
$this->assertSame($notices, DB::table('partnership_notifications')->count());
}
public function test_replay_does_not_revalidate_retired_type_or_criteria_as_a_new_write(): void
{
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
config(['partnerships.types' => [], 'partnerships.criteria' => []]);
$this->putJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', true);
}
public function test_changed_payload_with_the_same_operation_key_is_a_conflict_without_new_events(): void
{
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
$events = DB::table('partnership_events')->count();
$this->putJson($this->url(), [...$data, 'title' => 'SYNTHETIC changed content'])->assertConflict();
$this->assertSame($events, DB::table('partnership_events')->count());
$this->assertSame('SYNTHETIC saved revision', DB::table('portal_partnerships')->where('id', $this->record->id)->value('title'));
}
public function test_updates_use_distinct_operation_keys_without_changing_the_creation_identity(): void
{
$first = $this->command(['client_key' => (string) Str::uuid()]);
$this->putJson($this->url(), $first)->assertOk()->assertJsonPath('replayed', false);
$second = $this->command(['version' => $first['version'] + 1, 'title' => 'SYNTHETIC second revision', 'client_key' => $first['client_key']]);
$this->putJson($this->url(), $second)->assertOk()->assertJsonPath('saved_version', $second['version'] + 1);
$this->assertNotSame($first['operation_key'], $second['operation_key']);
$this->assertSame($this->record->client_key, DB::table('portal_partnerships')->where('id', $this->record->id)->value('client_key'));
}
public function test_authority_is_required_for_recovery_even_for_a_revoked_original_author(): void
{
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC editing organization', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $org, 'role' => 'officer']);
DB::table('portal_partnerships')->where('id', $this->record->id)->update(['organization_id' => $org]);
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
DB::table('memberships')->where('user_id', $this->owner->id)->where('organization_id', $org)->update(['role' => 'viewer']);
$this->putJson($this->url(), $data)->assertForbidden();
$this->get($this->url().'/editar')->assertForbidden();
DB::table('memberships')->where('user_id', $this->owner->id)->where('organization_id', $org)->update(['deleted_at' => now()]);
$this->putJson($this->url(), $data)->assertForbidden();
}
public function test_editor_exposes_only_the_current_actors_save_keys_and_not_private_hashes(): void
{
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
$url = $this->url().'/editar';
$version = app(HandleInertiaRequests::class)->version(Request::create($url));
$response = $this->get($url, ['X-Inertia' => 'true', 'X-Inertia-Version' => $version ?? ''])->assertOk()->assertJsonPath('props.editorActorId', $this->owner->id)->assertJsonPath('props.saveCommands.0.key', $data['operation_key'])->assertJsonPath('props.saveCommands.0.version', $data['version'] + 1);
foreach (['operation_hash', 'command_hash', '_creation_hash', '_notification_context'] as $private) {
$this->assertStringNotContainsString($private, $response->getContent());
}
$org = (string) Str::uuid();
$other = User::factory()->create();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC shared draft', 'organization_type' => 'private']);
DB::table('memberships')->insert([['user_id' => $this->owner->id, 'organization_id' => $org, 'role' => 'officer'], ['user_id' => $other->id, 'organization_id' => $org, 'role' => 'officer']]);
DB::table('portal_partnerships')->where('id', $this->record->id)->update(['organization_id' => $org]);
$this->actingAs($other)->get($url, ['X-Inertia' => 'true', 'X-Inertia-Version' => $version ?? ''])->assertOk()->assertJsonPath('props.saveCommands', [])->assertJsonPath('props.editorActorId', $other->id);
}
public function test_retired_sector_blocks_a_new_edit_but_not_a_read_only_replay(): void
{
$sector = Sector::create(['name' => 'SYNTHETIC edit sector', 'slug' => 'synthetic-'.Str::uuid(), 'is_active' => true]);
$data = $this->command(['sector_id' => $sector->id]);
$this->putJson($this->url(), $data)->assertOk();
$sector->update(['is_active' => false]);
$this->putJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', true);
$this->putJson($this->url(), [...$data, 'operation_key' => (string) Str::uuid(), 'version' => $data['version'] + 1])->assertUnprocessable();
}
public function test_invalid_or_stale_uncommitted_attempts_do_not_consume_a_key(): void
{
$data = $this->command();
$events = DB::table('partnership_events')->count();
$this->putJson($this->url(), [...$data, 'operation_key' => 'invalid'])->assertUnprocessable();
$this->putJson($this->url(), [...$data, 'version' => $data['version'] + 9])->assertConflict();
$this->assertSame($events, DB::table('partnership_events')->count());
$this->putJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', false);
}
public function test_operation_uuid_case_and_unused_creation_key_do_not_change_the_committed_edit(): void
{
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
$this->putJson($this->url(), [...$data, 'operation_key' => strtoupper($data['operation_key']), 'client_key' => (string) Str::uuid()])->assertOk()->assertJsonPath('replayed', true);
}
public function test_omitted_organization_and_payload_are_preserved_and_explicit_removal_is_a_new_command(): void
{
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC source organization', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $org, 'role' => 'officer']);
DB::table('portal_partnerships')->where('id', $this->record->id)->update(['organization_id' => $org]);
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
$saved = app(PartnershipExchange::class)->find($this->record->id);
$this->assertSame($org, $saved->organization_id);
$this->assertTrue(json_decode($saved->payload, true)['consent']);
$this->putJson($this->url(), [...$data, 'organization_id' => null])->assertConflict();
$this->putJson($this->url(), [...$data, 'operation_key' => (string) Str::uuid(), 'version' => $saved->version, 'organization_id' => null])->assertOk();
$this->assertNull(app(PartnershipExchange::class)->find($saved->id)->organization_id);
}
public function test_legacy_edits_without_operation_keys_keep_version_conflicts_instead_of_a_false_replay(): void
{
$data = $this->command();
unset($data['operation_key']);
$this->putJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', false)->assertJsonPath('saved_version', null);
$this->putJson($this->url(), $data)->assertConflict();
}
public function test_operation_keys_are_scoped_to_the_actor_and_partnership(): void
{
$data = $this->command();
$this->putJson($this->url(), $data)->assertOk();
$other = User::factory()->create();
$record = app(PartnershipExchange::class)->save($other, ['client_key' => (string) Str::uuid(), 'kind' => 'seeking']);
$this->actingAs($other)->putJson('/parcerias/'.$record->id,[...$data, 'version' => $record->version])->assertOk()->assertJsonPath('replayed',false);
$this->putJson($this->url(),$data)->assertForbidden();
}
}