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

172 lines
10 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 PartnershipPublicationSummaryTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private User $moderator;
private object $record;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Http::fake();
Mail::fake();
Queue::fake();
$this->owner = User::factory()->create();
$this->moderator = User::factory()->create();
config(['portal.managers' => [$this->moderator->email]]);
$this->record = app(PartnershipExchange::class)->save($this->owner, ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC original history title', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC original offer', 'payload' => ['consent' => true, 'confidentiality' => 'private', 'mandatory' => ['capacity']]]);
$this->actingAs($this->owner);
}
private function url(?int $id = null): string
{
return '/parcerias/'.($id ?? $this->record->id).'/historico';
}
private function edit(array $data = []): object
{
$current = app(PartnershipExchange::class)->find($this->record->id);
return app(PartnershipExchange::class)->save($this->owner, ['version' => $current->version, 'kind' => 'seeking', ...$data], $current->id);
}
private function organization(User $person, string $role = 'officer'): string
{
$id = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $id, 'name' => 'SYNTHETIC versions organization', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $person->id, 'organization_id' => $id, 'role' => $role]);
return $id;
}
public function test_detail_exposes_a_bounded_summary_instead_of_raw_event_rows(): void
{
for ($i = 0; $i < 9; $i++) {
$this->edit(['title' => 'SYNTHETIC revision '.$i]);
}
$response = $this->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonMissingPath('history')->assertJsonPath('historySummary.limit', 5)->assertJsonCount(5, 'historySummary.entries')->assertJsonPath('historySummary.has_more', true);
$entries = $response->json('historySummary.entries');
$current = app(PartnershipExchange::class)->find($this->record->id);
$this->assertSame([(int) $current->version, (int) $current->version - 1, (int) $current->version - 2, (int) $current->version - 3, (int) $current->version - 4], array_column($entries, 'version'));
$this->getJson($response->json('historySummary.href'))->assertOk()->assertJsonCount(10, 'entries');
}
public function test_summary_metadata_and_states_match_the_authorized_full_reader_without_snapshots(): void
{
$record = $this->edit(['title' => 'SYNTHETIC newer title']);
app(PartnershipExchange::class)->transition($this->owner, $record->id, $record->version, 'submit', 'SYNTHETIC reviewed reason');
$summary = $this->getJson('/parcerias/'.$this->record->id)->assertOk()->json('historySummary');
$full = $this->getJson($this->url())->assertOk()->json('entries');
foreach ($summary['entries'] as $index => $entry) {
foreach (['version', 'restricted', 'action', 'actor_name', 'occurred_at', 'reason'] as $field) {
$this->assertSame($full[$index][$field], $entry[$field]);
}
$this->assertSame(($full[$index]['previous']['availability'] === 'available' ? $full[$index]['previous']['status'] ?? null : null), $entry['status_before']);
$this->assertSame(($full[$index]['after']['availability'] === 'available' ? $full[$index]['after']['status'] ?? null : null), $entry['status_after']);
}
foreach (['event_type', 'actor_id', 'previous', 'after', 'payload', 'offer', 'client_key', 'operation_hash'] as $private) {
$this->assertStringNotContainsString('"'.$private.'"', json_encode($summary));
}
$this->assertSame('SYNTHETIC reviewed reason', $summary['entries'][0]['reason']);
$this->assertFalse($summary['has_more']);
}
public function test_a_new_organization_representative_does_not_receive_old_actor_or_decision_metadata(): void
{
$a = $this->organization($this->owner);
$this->edit(['organization_id' => $a]);
$oldActor = User::factory()->create(['name' => 'SYNTHETIC OLD PRIVATE ACTOR']);
DB::table('memberships')->insert(['user_id' => $oldActor->id, 'organization_id' => $a, 'role' => 'officer']);
$current = app(PartnershipExchange::class)->find($this->record->id);
app(PartnershipExchange::class)->save($oldActor, ['version' => $current->version, 'kind' => 'seeking', 'title' => 'SYNTHETIC A draft'], $current->id);
$b = $this->organization($this->owner);
$this->edit(['organization_id' => $b]);
$viewer = User::factory()->create();
DB::table('memberships')->insert(['user_id' => $viewer->id, 'organization_id' => $b, 'role' => 'viewer']);
$response = $this->actingAs($viewer)->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonPath('canEdit', false)
->assertJsonPath('historySummary.entries.0.restricted', false)->assertJsonPath('historySummary.entries.0.status_before', null)
->assertJsonPath('historySummary.entries.1.restricted', true)->assertJsonPath('historySummary.entries.1.actor_name', null)
->assertJsonPath('historySummary.entries.1.occurred_at', null)->assertJsonPath('historySummary.entries.1.action', null)->assertJsonPath('historySummary.entries.1.reason', null);
$this->assertStringNotContainsString('OLD PRIVATE ACTOR', $response->getContent());
$this->postJson('/parcerias/'.$this->record->id.'/estado', ['version' => 99, 'action' => 'archive', 'reason' => 'SYNTHETIC viewer cannot decide'])->assertForbidden();
DB::table('memberships')->where('user_id', $viewer->id)->update(['deleted_at' => now()]);
$this->getJson('/parcerias/'.$this->record->id)->assertNotFound();
}
public function test_moderation_keeps_its_existing_historical_access(): void
{
$a = $this->organization($this->owner);
$this->edit(['organization_id' => $a]);
$b = $this->organization($this->owner);
$this->edit(['organization_id' => $b]);
$this->actingAs($this->moderator)->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonPath('moderator', true)
->assertJsonPath('historySummary.entries.1.restricted', false)->assertJsonPath('historySummary.entries.1.actor_name', $this->owner->name);
}
public function test_public_and_candidate_reads_have_no_historical_summary_even_after_publication(): void
{
$exchange = app(PartnershipExchange::class);
$record = $exchange->transition($this->owner, $this->record->id, $this->record->version, 'submit', 'SYNTHETIC submit');
$record = $exchange->transition($this->moderator, $record->id, $record->version, 'publish', 'SYNTHETIC publish');
$candidate = User::factory()->create();
$exchange->interest($candidate, $record->id, ['proposal' => 'SYNTHETIC candidacy', 'payload' => ['consent' => true]]);
$this->actingAs($candidate)->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('historySummary', null)->assertJsonMissingPath('history');
$this->getJson($this->url())->assertForbidden();
auth()->forgetGuards();
$this->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('historySummary', null)->assertJsonMissingPath('history');
}
public function test_legacy_scope_unknown_does_not_become_current_authority_or_an_inferred_action(): void
{
$event = DB::table('partnership_events')->where('partnership_id', $this->record->id)->first();
$payload = json_decode($event->payload, true);
unset($payload['payload']['previous'],$payload['payload']['applied'],$payload['payload']['_notification_context']);
$payload['payload']['reason'] = 'SYNTHETIC RESERVED REASON';
DB::table('partnership_events')->where('id', $event->id)->update(['payload' => json_encode($payload)]);
$result = $this->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonPath('historySummary.entries.0.restricted', true)
->assertJsonPath('historySummary.entries.0.action', null)->assertJsonPath('historySummary.entries.0.status_after', null)->assertJsonPath('historySummary.entries.0.actor_name', null);
$this->assertStringNotContainsString('RESERVED REASON', $result->getContent());
}
public function test_no_publication_events_is_an_explicit_empty_summary_with_a_valid_history_link(): void
{
DB::table('partnership_events')->where('partnership_id', $this->record->id)->update(['event_type' => 'synthetic_unrelated.v1']);
$response = $this->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonCount(0, 'historySummary.entries')->assertJsonPath('historySummary.has_more', false)
->assertJsonPath('historySummary.href', '/parcerias/'.$this->record->id.'/historico');
$this->getJson($response->json('historySummary.href'))->assertOk()->assertJsonCount(0, 'entries');
}
public function test_native_and_json_summary_reads_do_not_change_versions_audit_or_notifications(): void
{
$before = DB::table('portal_partnerships')->where('id', $this->record->id)->first();
$events = DB::table('partnership_events')->count();
$audits = DB::table('audit_events')->count();
$notices = DB::table('partnership_notifications')->count();
$this->get('/parcerias/'.$this->record->id)->assertOk()->assertInertia(fn (Assert $p) => $p->component('partnerships/detail')->where('historySummary.limit', 5)->missing('history'));
$this->getJson('/parcerias/'.$this->record->id)->assertOk();
$this->assertEquals($before, DB::table('portal_partnerships')->where('id', $this->record->id)->first());
$this->assertSame($events, DB::table('partnership_events')->count());
$this->assertSame($audits, DB::table('audit_events')->count());
$this->assertSame($notices, DB::table('partnership_notifications')->count());
}
}