Files
Investidor-V2/tests/Feature/PartnershipAgreementProjectionTest.php
T

236 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\PartnershipExchange;
use Illuminate\Foundation\Testing\RefreshDatabase;
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 Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
final class PartnershipAgreementProjectionTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private User $candidate;
private int $roomId;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Http::fake();
Mail::fake();
Queue::fake();
$this->owner = User::factory()->create();
$this->candidate = User::factory()->create();
$moderator = User::factory()->create();
config(['portal.managers' => [$moderator->email]]);
$service = app(PartnershipExchange::class);
$record = $service->save($this->owner, ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC agreement', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC offer', 'payload' => ['consent' => true]]);
$record = $service->transition($this->owner, $record->id, $record->version, 'submit', 'SYNTHETIC submit');
$record = $service->transition($moderator, $record->id, $record->version, 'publish', 'SYNTHETIC publish');
$interest = $service->interest($this->candidate, $record->id, ['proposal' => 'SYNTHETIC candidacy', 'payload' => ['consent' => true]]);
$interest = $service->decideInterest($this->owner, $interest->id, $interest->version, 'shortlist', 'SYNTHETIC shortlist');
$service->decideInterest($this->owner, $interest->id, $interest->version, 'connect', 'SYNTHETIC connect');
$this->roomId = (int) DB::table('partnership_rooms')->where('interest_id', $interest->id)->value('id');
$this->actingAs($this->owner)->postJson($this->url(), ['version' => 1, 'action' => 'record', 'title' => 'SYNTHETIC instrument', 'terms' => 'SYNTHETIC terms', 'reason' => 'SYNTHETIC record'])->assertOk();
}
private function url(): string
{
return '/parcerias/salas/'.$this->roomId.'/acordo';
}
private function command(string $action = 'cancel'): array
{
$context = app(PartnershipExchange::class)->room($this->owner, $this->roomId);
return ['client_key' => (string) Str::uuid(), 'version' => $context['room']->version, 'partnership_version' => $context['partnership']->version, 'interest_version' => $context['interest']->version, 'action' => $action, 'reason' => 'SYNTHETIC documented action'];
}
public function test_current_room_does_not_disclose_versions_from_an_older_organization(): void
{
$a = (string) Str::uuid();
$b = (string) Str::uuid();
$viewer = User::factory()->create();
foreach ([$a, $b] as $org) {
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC organization', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $org, 'role' => 'officer']);
}
DB::table('memberships')->insert(['user_id' => $viewer->id, 'organization_id' => $b, 'role' => 'viewer']);
$exchange = app(PartnershipExchange::class);
$parent = $exchange->room($this->owner, $this->roomId)['partnership'];
$parent = $exchange->save($this->owner, ['version' => $parent->version, 'kind' => 'seeking', 'organization_id' => $a], $parent->id);
$this->postJson($this->url(), [...$this->command('record'), 'title' => 'SYNTHETIC OLD PRIVATE INSTRUMENT', 'terms' => 'SYNTHETIC OLD PRIVATE CONDITIONS'])->assertOk();
$exchange->save($this->owner, ['version' => $parent->version, 'kind' => 'seeking', 'organization_id' => $b], $parent->id);
$this->postJson($this->url(), [...$this->command('record'), 'title' => 'SYNTHETIC CURRENT INSTRUMENT', 'terms' => 'SYNTHETIC CURRENT CONDITIONS'])->assertOk();
$saved = DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement');
$response = $this->actingAs($viewer)->getJson('/parcerias/salas/'.$this->roomId)->assertOk()->assertJsonPath('room.agreement.title', 'SYNTHETIC CURRENT INSTRUMENT')->assertJsonMissingPath('room.agreement.previous_versions');
$this->assertStringNotContainsString('OLD PRIVATE', $response->getContent());
$this->assertSame($saved, DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'));
$this->getJson('/parcerias/salas/'.$this->roomId.'/historico')->assertOk()->assertJsonPath('entries.1.restricted', true)->assertJsonPath('entries.1.after.availability', 'restricted');
$this->actingAs($this->candidate)->getJson('/parcerias/salas/'.$this->roomId.'/historico')->assertOk()->assertJsonPath('entries.1.after.instrument.title', 'SYNTHETIC OLD PRIVATE INSTRUMENT');
}
private function roomUrl(): string
{
return '/parcerias/salas/'.$this->roomId;
}
private function instrument(): array
{
return json_decode(DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'), true);
}
private function storeInstrument(mixed $value): string
{
DB::table('partnership_rooms')->where('id', $this->roomId)->update(['agreement' => json_encode($value)]);
return DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement');
}
public function test_current_fields_and_named_confirmations_survive_while_private_extensions_do_not(): void
{
$this->postJson($this->url(), $this->command('confirm'))->assertOk();
$this->actingAs($this->candidate)->postJson($this->url(), $this->command('confirm'))->assertOk();
$instrument = $this->instrument();
$instrument['private_extension'] = ['secret' => 'SYNTHETIC INTERNAL SECRET'];
$instrument['confirmations']['proponent']['actor_name'] = 'SYNTHETIC FORGED NAME';
$instrument['confirmations']['proponent']['private_extension'] = 'SYNTHETIC INTERNAL SECRET';
$saved = $this->storeInstrument($instrument);
$response = $this->actingAs($this->owner)->getJson($this->roomUrl())->assertOk()
->assertJsonPath('room.status', 'agreed')->assertJsonPath('room.agreement.title', 'SYNTHETIC instrument')
->assertJsonPath('room.agreement.terms', 'SYNTHETIC terms')->assertJsonPath('room.agreement.legal_signature_status', 'not_verified')
->assertJsonPath('room.agreement.confirmations.proponent.actor_id', $this->owner->id)
->assertJsonPath('room.agreement.confirmations.candidate.actor_id', $this->candidate->id)
->assertJsonPath('room.agreement.confirmations.proponent.reason', 'SYNTHETIC documented action')->assertJsonCount(1, 'agreementCommands');
foreach (['previous_versions', 'recorded_context', 'private_extension', 'INTERNAL SECRET', 'FORGED NAME'] as $hidden) {
$this->assertStringNotContainsString($hidden, $response->getContent());
}
$this->assertSame($instrument['confirmations']['proponent']['date'], $response->json('room.agreement.confirmations.proponent.date'));
$this->assertSame($saved, DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'));
}
public function test_native_inertia_uses_the_same_current_projection_and_private_response(): void
{
$instrument = $this->instrument();
$instrument['previous_versions'] = [['title' => 'SYNTHETIC NATIVE PRIVATE']];
$instrument['private_extension'] = 'SYNTHETIC NATIVE PRIVATE';
$this->storeInstrument($instrument);
$this->get($this->roomUrl())->assertOk()->assertHeader('Cache-Control', 'no-store, private')
->assertInertia(fn (Assert $page) => $page->component('partnerships/room')->where('room.id', $this->roomId)
->where('room.agreement.title', 'SYNTHETIC instrument')->missing('room.agreement.previous_versions')->missing('room.agreement.recorded_context'))
->assertDontSee('NATIVE PRIVATE');
}
public function test_unrecognized_legacy_instruments_are_marked_without_echoing_opaque_content(): void
{
foreach (['SYNTHETIC OPAQUE PRIVATE', false, 123, ['SYNTHETIC OPAQUE PRIVATE']] as $value) {
$saved = $this->storeInstrument($value);
$response = $this->getJson($this->roomUrl())->assertOk()->assertJsonPath('room.agreement', 'unrecognized');
$this->assertStringNotContainsString('OPAQUE PRIVATE', $response->getContent());
$this->assertSame($saved, DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'));
}
$this->storeInstrument(null);
$this->getJson($this->roomUrl())->assertOk()->assertJsonPath('room.agreement', null);
}
public function test_missing_null_empty_and_malformed_confirmation_maps_remain_distinguishable(): void
{
$base = $this->instrument();
foreach ([null, [], (object) [], 'SYNTHETIC MAP PRIVATE', [['actor_id' => $this->owner->id]]] as $map) {
$saved = $this->storeInstrument([...$base, 'confirmations' => $map]);
$response = $this->getJson($this->roomUrl())->assertOk();
$projected = $response->json('room.agreement.confirmations');
if ($map === null) {
$this->assertNull($projected);
} elseif ($map === [] || $map instanceof \stdClass) {
$this->assertSame([], $projected);
} else {
$this->assertSame('unrecognized', $projected);
}
$this->assertStringNotContainsString('MAP PRIVATE', $response->getContent());
$this->assertSame($saved, DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'));
}
unset($base['confirmations']);
$this->storeInstrument($base);
$this->getJson($this->roomUrl())->assertOk()->assertJsonMissingPath('room.agreement.confirmations');
}
public function test_incomplete_stamps_and_unknown_references_do_not_disclose_their_raw_fields(): void
{
$value = $this->instrument();
$value['confirmations'] = ['proponent' => ['actor_id' => 'SYNTHETIC BAD PRIVATE', 'date' => ['SYNTHETIC BAD PRIVATE'], 'reason' => ['secret' => 'SYNTHETIC BAD PRIVATE']], 'SYNTHETIC UNKNOWN PRIVATE' => ['actor_id' => $this->candidate->id]];
$saved = $this->storeInstrument($value);
$response = $this->getJson($this->roomUrl())->assertOk()->assertJsonPath('room.agreement.confirmations.proponent.actor_id', null)
->assertJsonPath('room.agreement.confirmations.proponent.date', null)->assertJsonPath('room.agreement.confirmations.proponent.reason', null)
->assertJsonPath('room.agreement.confirmations._unrecognized_references', true)->assertJsonMissingPath('room.agreement.confirmations.candidate');
$this->assertStringNotContainsString('PRIVATE', $response->getContent());
$this->assertSame($saved, DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'));
}
public function test_reads_and_replay_do_not_sanitize_the_stored_aggregate_or_its_audit_snapshots(): void
{
$original = $this->instrument();
$original['private_extension'] = 'SYNTHETIC PRESERVED PRIVATE';
$original['previous_versions'] = [['title' => 'SYNTHETIC HISTORICAL PRIVATE']];
$this->storeInstrument($original);
$command = [...$this->command('record'), 'title' => 'SYNTHETIC new public-facing title', 'terms' => 'SYNTHETIC new terms'];
$this->postJson($this->url(), $command)->assertOk();
$saved = DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement');
$stored = $this->instrument();
$this->assertSame('SYNTHETIC PRESERVED PRIVATE', $stored['previous_versions'][1]['private_extension']);
$events = DB::table('partnership_events')->count();
$audits = DB::table('audit_events')->count();
$notices = DB::table('partnership_notifications')->count();
$this->getJson($this->roomUrl())->assertOk()->assertJsonMissingPath('room.agreement.previous_versions');
$this->postJson($this->url(), $command)->assertOk()->assertJsonPath('replayed', true);
$this->assertSame($saved, DB::table('partnership_rooms')->where('id', $this->roomId)->value('agreement'));
$this->assertSame($events, DB::table('partnership_events')->count());
$this->assertSame($audits, DB::table('audit_events')->count());
$this->assertSame($notices, DB::table('partnership_notifications')->count());
$event = DB::table('partnership_events')->where('event_type', 'partnership_agreement.changed.v1')->orderByDesc('aggregate_version')->first();
$data = json_decode($event->payload, true)['payload'];
$this->assertSame('SYNTHETIC PRESERVED PRIVATE', $data['previous']['agreement']['private_extension']);
$this->assertSame($stored, $data['applied']['agreement']);
}
public function test_current_representation_and_private_room_access_still_apply(): void
{
$outsider = User::factory()->create();
config(['portal.managers' => [$outsider->email]]);
$this->actingAs($outsider)->getJson($this->roomUrl())->assertForbidden();
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC access', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $org, 'role' => 'viewer']);
DB::table('portal_partnerships')->where('id', app(PartnershipExchange::class)->room($this->owner, $this->roomId)['partnership']->id)->update(['organization_id' => $org]);
$this->actingAs($this->owner)->getJson($this->roomUrl())->assertOk()->assertJsonPath('canWrite', false)->assertJsonPath('agreementCommands', []);
$this->postJson($this->url(), $this->command('confirm'))->assertForbidden();
DB::table('memberships')->where('user_id', $this->owner->id)->update(['deleted_at' => now()]);
$this->getJson($this->roomUrl())->assertForbidden();
$this->actingAs($this->candidate)->getJson($this->roomUrl())->assertOk();
}
public function test_other_room_consumers_link_to_the_room_without_embedding_its_instrument_history(): void
{
$value = $this->instrument();
$value['previous_versions'] = [['title' => 'SYNTHETIC OTHER CONSUMER PRIVATE']];
$this->storeInstrument($value);
$parent = app(PartnershipExchange::class)->room($this->owner, $this->roomId)['partnership'];
$response = $this->getJson('/parcerias/'.$parent->id)->assertOk()->assertJsonPath('interests.0.room_id', $this->roomId);
$this->assertStringNotContainsString('OTHER CONSUMER PRIVATE', $response->getContent());
$this->actingAs($this->candidate)->get('/parcerias/painel')->assertOk()
->assertInertia(fn (Assert $page) => $page->component('partnerships/dashboard')->where('interests.0.room_id', $this->roomId))
->assertDontSee('OTHER CONSUMER PRIVATE');
}
}