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()); } }