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_creation_edit_and_transition_have_real_before_and_after_versions_without_a_placeholder(): void { $edit = $this->edit(['title' => 'SYNTHETIC revised title']); app(PartnershipExchange::class)->transition($this->owner, $edit->id, $edit->version, 'submit', 'SYNTHETIC reviewed content', (string) Str::uuid()); $this->getJson($this->url())->assertOk()->assertJsonCount(3, 'entries')->assertJsonPath('entries.0.action', 'submit')->assertJsonPath('entries.0.previous.fields.title', 'SYNTHETIC revised title')->assertJsonPath('entries.0.after.status', 'submitted')->assertJsonPath('entries.1.previous.fields.title', 'SYNTHETIC original history title')->assertJsonPath('entries.1.after.fields.title', 'SYNTHETIC revised title')->assertJsonPath('entries.2.action', 'create')->assertJsonPath('entries.2.previous.availability', 'not_recorded')->assertJsonPath('entries.2.after.version', $this->record->version); } public function test_publication_is_not_historical_access_for_another_candidate_or_anonymous_reader(): void { $service = app(PartnershipExchange::class); $record = $service->transition($this->owner, $this->record->id, $this->record->version, 'submit', 'SYNTHETIC submit'); $record = $service->transition($this->moderator, $record->id, $record->version, 'publish', 'SYNTHETIC publish'); $candidate = User::factory()->create(); $service->interest($candidate, $record->id, ['proposal' => 'SYNTHETIC candidate', 'payload' => ['consent' => true]]); $this->actingAs($candidate)->getJson($this->url())->assertForbidden(); $this->actingAs($this->moderator)->getJson($this->url())->assertOk(); auth()->forgetGuards(); $this->getJson($this->url())->assertUnauthorized(); } public function test_changing_organizations_does_not_transfer_the_prior_private_snapshot(): void { $orgA = $this->organization($this->owner); $this->edit(['organization_id' => $orgA, 'offer' => 'SYNTHETIC A-only offer']); $orgB = $this->organization($this->owner); $this->edit(['organization_id' => $orgB, 'offer' => 'SYNTHETIC B offer']); $viewer = User::factory()->create(); DB::table('memberships')->insert(['user_id' => $viewer->id, 'organization_id' => $orgB, 'role' => 'viewer']); $response = $this->actingAs($viewer)->getJson($this->url())->assertOk()->assertJsonPath('entries.0.after.fields.offer', 'SYNTHETIC B offer')->assertJsonPath('entries.0.previous.availability', 'restricted')->assertJsonPath('entries.1.restricted', true)->assertJsonPath('entries.2.restricted', true); $this->assertStringNotContainsString('SYNTHETIC A-only offer', $response->getContent()); $this->postJson('/parcerias/'.$this->record->id.'/estado', ['version' => $this->record->version + 2, 'action' => 'archive', 'reason' => 'SYNTHETIC unauthorized action'])->assertForbidden(); $this->actingAs($this->moderator)->getJson($this->url())->assertOk()->assertJsonPath('entries.0.previous.fields.offer', 'SYNTHETIC A-only offer'); } public function test_revoked_author_and_deleted_organization_do_not_keep_owner_history_access(): void { $org = $this->organization($this->owner); $this->edit(['organization_id' => $org]); DB::table('memberships')->where('user_id', $this->owner->id)->where('organization_id', $org)->update(['deleted_at' => now()]); $this->getJson($this->url())->assertForbidden(); DB::table('memberships')->where('user_id', $this->owner->id)->where('organization_id', $org)->update(['deleted_at' => null]); DB::table('organizations')->where('id', $org)->update(['deleted_at' => now()]); $this->getJson($this->url())->assertForbidden(); $this->actingAs($this->moderator)->getJson($this->url())->assertOk(); } public function test_legacy_unknown_scope_is_partial_for_moderation_and_not_reconstructed_from_current_content(): void { app(PartnershipExchange::class)->event($this->record, $this->owner, 'partnership_opportunity.changed.v1', ['previous_status' => 'published'], 'SYNTHETIC legacy event'); DB::table('portal_partnerships')->where('id', $this->record->id)->update(['title' => 'SYNTHETIC current-only private title']); $this->getJson($this->url())->assertOk()->assertJsonPath('entries.0.restricted', true)->assertJsonPath('entries.0.after.availability', 'scope_unknown'); $response = $this->actingAs($this->moderator)->getJson($this->url())->assertOk()->assertJsonPath('entries.0.action', null)->assertJsonPath('entries.0.previous.status', 'published')->assertJsonPath('entries.0.previous.version', null)->assertJsonPath('entries.0.after.complete', false)->assertJsonPath('entries.0.after.status', null)->assertJsonPath('entries.0.after.scope_known', false); $this->assertArrayNotHasKey('title', $response->json('entries.0.after.fields')); $this->assertStringNotContainsString('SYNTHETIC current-only private title', $response->getContent()); } public function test_known_legacy_context_authorizes_only_a_partial_recorded_state_and_reason(): void { DB::table('portal_partnerships')->where('id', $this->record->id)->update(['status' => 'returned']); $record = app(PartnershipExchange::class)->find($this->record->id); app(PartnershipExchange::class)->event($record, $this->owner, 'partnership_opportunity.returned.v1', ['from' => 'submitted', 'to' => 'returned', 'reason' => 'SYNTHETIC legacy recorded reason'], 'SYNTHETIC legacy notice', [$this->owner->id]); $this->getJson($this->url())->assertOk()->assertJsonPath('entries.0.reason', 'SYNTHETIC legacy recorded reason')->assertJsonPath('entries.0.action', null)->assertJsonPath('entries.0.after.status', 'returned')->assertJsonPath('entries.0.after.complete', false)->assertJsonPath('entries.0.previous.availability', 'scope_unknown'); } public function test_pagination_is_stable_when_new_versions_arrive_and_never_includes_candidacy_events(): void { for ($i = 1; $i <= 20; $i++) { $this->edit(['title' => 'SYNTHETIC revision '.$i]); }app(PartnershipExchange::class)->event(app(PartnershipExchange::class)->find($this->record->id), $this->owner, 'partnership_interest.changed.v1', [], 'SYNTHETIC other aggregate'); $first = $this->getJson($this->url())->assertOk()->assertJsonCount(10, 'entries')->assertJsonPath('entries.0.version', 22)->assertJsonPath('entries.9.version', 13); $next = $first->json('next_url'); $this->edit(['title' => 'SYNTHETIC later revision']); $second = $this->getJson($next)->assertOk()->assertJsonCount(10, 'entries')->assertJsonPath('entries.0.version', 12)->assertJsonPath('entries.9.version', 3); $this->getJson($second->json('next_url'))->assertOk()->assertJsonCount(1, 'entries')->assertJsonPath('entries.0.version', 2)->assertJsonPath('next_url', null); $this->getJson($this->url())->assertOk()->assertJsonPath('entries.0.version', 23); } public function test_cursor_is_bound_to_actor_process_and_purpose_and_never_grants_access_after_revocation(): void { $org = $this->organization($this->owner); $this->edit(['organization_id' => $org]); for ($i = 0; $i < 10; $i++) { $this->edit(['title' => 'SYNTHETIC cursor revision '.$i]); }$cursor = $this->getJson($this->url())->assertOk()->json('next_url'); $this->assertIsString($cursor); $this->actingAs($this->moderator)->getJson($cursor)->assertUnprocessable(); $this->actingAs($this->owner); $other = app(PartnershipExchange::class)->save($this->owner, ['client_key' => (string) Str::uuid(), 'kind' => 'seeking']); $this->getJson($this->url($other->id).'?'.parse_url($cursor, PHP_URL_QUERY))->assertUnprocessable(); $this->getJson($this->url().'?before=invalid-token')->assertUnprocessable(); $token = Crypt::encryptString(json_encode(['actor' => $this->owner->id, 'interest' => $this->record->id, 'before' => 999])); $this->getJson($this->url().'?before='.rawurlencode($token))->assertUnprocessable(); DB::table('memberships')->where('organization_id', $org)->where('user_id', $this->owner->id)->update(['deleted_at' => now()]); $this->getJson($cursor)->assertForbidden(); } public function test_native_history_is_read_only_private_and_does_not_serialize_command_or_event_envelopes(): void { app(PartnershipExchange::class)->transition($this->owner, $this->record->id, $this->record->version, 'archive', 'SYNTHETIC archive', (string) Str::uuid()); $row = 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(); $version = app(HandleInertiaRequests::class)->version(Request::create($this->url())); $response = $this->get($this->url(), ['X-Inertia' => 'true', 'X-Inertia-Version' => $version ?? ''])->assertOk()->assertJsonPath('component', 'partnerships/publication-history')->assertJsonPath('encryptHistory', true); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('no-store')); foreach (['command_hash', 'client_key', 'aggregate_version', '_notification_context', '_creation_hash'] as $private) { $this->assertStringNotContainsString($private, $response->getContent()); } $this->assertEquals($row, 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()); } public function test_recorded_references_and_declarations_survive_current_catalogue_changes_and_explicit_clearing(): void { $place = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $place, 'code' => $place, 'name' => 'SYNTHETIC location', 'location_type' => 'country', 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:synthetic-'.substr($place, 0, 8)]); $project = Project::factory()->create(); InvestmentDossier::create(['project_id' => $project->id, 'title' => 'SYNTHETIC source', 'source' => 'Synthetic fixture only', 'content' => [], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'data_classification' => 'public', 'approval_basis' => 'document_owner', 'approval_evidence' => 'Synthetic QA fixture only']); $sector = Sector::create(['name' => 'SYNTHETIC sector', 'slug' => 'synthetic-'.Str::uuid(), 'is_active' => true]); $this->edit(['sector_id' => $sector->id, 'origins' => [['type' => 'project', 'target_id' => $project->id]], 'locations' => [['geo_location_id' => $place, 'role' => 'coverage', 'source_reference' => 'SYNTHETIC original declared source']]]); $this->edit(['sector_id' => null, 'offer' => null, 'origins' => [], 'locations' => [], 'payload' => ['consent' => false, 'mandatory' => []]]); DB::table('geo_locations')->where('id', $place)->update(['name' => 'SYNTHETIC renamed current location']); $sector->update(['is_active' => false]); $response = $this->getJson($this->url())->assertOk()->assertJsonPath('entries.0.previous.sector_id', (string) $sector->id)->assertJsonPath('entries.0.previous.origins.0.target_id', $project->id)->assertJsonPath('entries.0.previous.locations.0.source_reference', 'SYNTHETIC original declared source')->assertJsonPath('entries.0.after.fields.offer', '')->assertJsonPath('entries.0.after.consent', false)->assertJsonPath('entries.0.after.mandatory_known', true)->assertJsonPath('entries.0.after.mandatory', [])->assertJsonPath('entries.0.after.origins', []); $this->assertStringNotContainsString('SYNTHETIC renamed current location', $response->getContent()); } public function test_malformed_scope_or_references_are_unknown_instead_of_a_different_proposal_or_an_empty_source_list(): void { $event = DB::table('partnership_events')->where('partnership_id', $this->record->id)->first(); $payload = json_decode($event->payload, true); $original = $payload['payload']['applied']; $payload['payload']['applied']['id'] = $this->record->id + 999; DB::table('partnership_events')->where('id', $event->id)->update(['payload' => json_encode($payload)]); $this->actingAs($this->moderator)->getJson($this->url())->assertOk()->assertJsonPath('entries.0.after.availability', 'scope_unknown'); $payload['payload']['applied'] = $original; $payload['payload']['applied']['origins'] = [['invalid' => true]]; $payload['payload']['applied']['locations'] = [['geo_location_id' => 'invalid identity']]; $payload['payload']['applied']['sector_id'] = ['invalid']; DB::table('partnership_events')->where('id', $event->id)->update(['payload' => json_encode($payload)]); $this->getJson($this->url())->assertOk()->assertJsonPath('entries.0.after.origins_incomplete', true)->assertJsonPath('entries.0.after.locations_incomplete', true)->assertJsonPath('entries.0.after.complete', false)->assertJsonPath('entries.0.after.sector_recorded', false); } public function test_repeating_creation_does_not_add_a_false_revision_or_rewrite_a_snapshot(): void { $service = app(PartnershipExchange::class); $input = ['client_key' => (string) Str::uuid(), 'kind' => 'seeking', 'title' => 'SYNTHETIC replay']; $record = $service->save($this->owner, $input); $before = DB::table('partnership_events')->where('partnership_id', $record->id)->first(); $service->save($this->owner, $input); $this->assertSame(1, DB::table('partnership_events')->where('partnership_id', $record->id)->count()); $this->assertEquals($before, DB::table('partnership_events')->where('partnership_id', $record->id)->first()); } }