withoutVite(); Http::fake(); Mail::fake(); Queue::fake(); Storage::fake('local'); $this->owner = User::factory()->create(); $this->candidate = User::factory()->create(); $service = app(PartnershipExchange::class); $record = $service->save($this->owner, ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC recovery', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC offer', 'seeking' => 'SYNTHETIC cooperation', 'payload' => ['consent' => true]]); $record = $service->transition($this->owner, $record->id, $record->version, 'submit', 'SYNTHETIC submission'); $moderator = User::factory()->create(); config(['portal.managers' => [$moderator->email]]); $service->transition($moderator, $record->id, $record->version, 'publish', 'SYNTHETIC moderation'); $interest = $service->interest($this->candidate, $record->id, ['proposal' => 'SYNTHETIC proposal', '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 connection'); $this->roomId = (int) DB::table('partnership_rooms')->where('interest_id', $interest->id)->value('id'); $this->actingAs($this->owner)->postJson($this->url('acordo'), ['version' => 1, 'action' => 'record', 'title' => 'SYNTHETIC plan', 'terms' => 'SYNTHETIC terms', 'reason' => 'SYNTHETIC record'])->assertOk(); } private function url(string $suffix = ''): string { return '/parcerias/salas/'.$this->roomId.($suffix ? '/'.$suffix : ''); } private function milestone(): array { return ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC deliverable', 'responsible_id' => $this->candidate->id, 'due_date' => null, 'status' => 'pending', 'evidence' => null]; } private function upload(string $key, bool $shared = false, string $content = 'SYNTHETIC PDF', string $name = 'synthetic-proof.pdf'): array { return ['client_key' => $key, 'shared' => $shared, 'file' => UploadedFile::fake()->createWithContent($name, "%PDF-1.4\n".$content."\n%%EOF")]; } private function closeRoom(): void { $this->postJson($this->url('acordo'), ['version' => 2, 'action' => 'cancel', 'reason' => 'SYNTHETIC closed while response uncertain'])->assertOk(); } public function test_creation_replay_has_one_milestone_event_audit_and_notification_set(): void { $input = $this->milestone(); $events = DB::table('partnership_events')->count(); $audits = DB::table('audit_events')->count(); $notifications = DB::table('partnership_notifications')->count(); $first = $this->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('replayed', false)->json('milestone_id'); $this->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('milestone_id', $first)->assertJsonPath('version', 1); $this->postJson($this->url('marcos'), [...$input, 'client_key' => strtoupper($input['client_key'])])->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('milestone_id', $first); $this->assertDatabaseCount('partnership_milestones', 1); $this->assertSame($events + 1, DB::table('partnership_events')->count()); $this->assertSame($audits + 1, DB::table('audit_events')->count()); $this->assertSame($notifications + 2, DB::table('partnership_notifications')->count()); $this->getJson($this->url())->assertOk()->assertJsonPath('milestones.0.client_key', $input['client_key']); $this->actingAs($this->candidate)->getJson($this->url())->assertOk()->assertJsonPath('milestones.0.client_key', null); } public function test_creation_replay_after_update_and_closure_never_rewrites_the_milestone(): void { $input = $this->milestone(); $id = $this->postJson($this->url('marcos'), $input)->assertOk()->json('milestone_id'); $this->putJson($this->url('marcos/'.$id), [...$input, 'title' => 'SYNTHETIC revised deliverable', 'version' => 1])->assertOk(); $this->closeRoom(); $events = DB::table('partnership_events')->count(); $this->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('milestone_id', $id)->assertJsonPath('version', 2)->assertJsonPath('replayed', true); $this->assertDatabaseHas('partnership_milestones', ['id' => $id, 'title' => 'SYNTHETIC revised deliverable', 'version' => 2]); $this->postJson($this->url('marcos'), [...$input, 'client_key' => (string) Str::uuid()])->assertConflict(); $this->assertSame($events, DB::table('partnership_events')->count()); } public function test_milestone_keys_are_content_bound_scoped_per_actor_and_legacy_requests_still_work(): void { $input = $this->milestone(); $this->postJson($this->url('marcos'), $input)->assertOk(); $this->postJson($this->url('marcos'), [...$input, 'title' => 'SYNTHETIC different command'])->assertConflict(); $this->postJson($this->url('marcos'), [...$input, 'client_key' => 'not-a-uuid'])->assertUnprocessable(); $this->actingAs($this->candidate)->postJson($this->url('marcos'), $input)->assertOk()->assertJsonPath('replayed', false); unset($input['client_key']); $this->postJson($this->url('marcos'), $input)->assertOk(); $this->assertDatabaseCount('partnership_milestones', 3); } public function test_recovery_does_not_restore_revoked_representation_or_allow_a_read_only_actor(): void { $input = $this->milestone(); $this->postJson($this->url('marcos'), $input)->assertOk(); $record = DB::table('partnership_rooms as r')->join('partnership_interests as i', 'i.id', '=', 'r.interest_id')->where('r.id', $this->roomId)->value('i.partnership_id'); $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC new representation', 'organization_type' => 'private']); DB::table('portal_partnerships')->where('id', $record)->update(['organization_id' => $org]); DB::table('memberships')->insert(['organization_id' => $org, 'user_id' => $this->owner->id, 'role' => 'viewer']); $this->getJson($this->url())->assertOk()->assertJsonPath('canWrite', false); $this->postJson($this->url('marcos'), $input)->assertForbidden(); DB::table('memberships')->where('user_id', $this->owner->id)->update(['deleted_at' => now()]); $this->postJson($this->url('marcos'), $input)->assertForbidden(); $this->assertDatabaseCount('partnership_milestones', 1); } public function test_upload_replay_preserves_original_bytes_one_link_and_one_scan_dispatch(): void { $key = (string) Str::uuid(); $events = DB::table('partnership_events')->count(); $id = $this->postJson($this->url('documentos'), $this->upload($key))->assertOk()->assertJsonPath('replayed', false)->json('document_id'); $this->postJson($this->url('documentos'), $this->upload($key))->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('document_id', $id); $this->postJson($this->url('documentos'), $this->upload(strtoupper($key)))->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('document_id', $id); $this->postJson($this->url('documentos'), $this->upload($key, true))->assertConflict(); $this->postJson($this->url('documentos'), $this->upload($key, false, 'SYNTHETIC different bytes'))->assertConflict(); $this->postJson($this->url('documentos'), $this->upload($key, false, 'SYNTHETIC PDF', 'renamed.pdf'))->assertConflict(); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('partnership_documents', 1); $this->assertCount(1, Storage::disk('local')->allFiles()); Queue::assertPushed(ScanDocumentForMalware::class, 1); $this->assertSame($events + 1, DB::table('partnership_events')->count()); $document = DB::table('documents')->where('id', $id)->first(); $this->assertSame('quarantined', $document->status); $this->assertSame($document->sha256, hash_file('sha256', Storage::disk('local')->path($document->storage_key))); } public function test_upload_recovery_after_revocation_and_closure_does_not_reshare_or_requeue(): void { $key = (string) Str::uuid(); $id = $this->postJson($this->url('documentos'), $this->upload($key, true))->assertOk()->json('document_id'); $link = DB::table('partnership_documents')->where('document_id', $id)->first(); $this->postJson($this->url('documentos/'.$link->id.'/partilha'), ['shared' => false])->assertOk(); $this->closeRoom(); $events = DB::table('partnership_events')->count(); $this->postJson($this->url('documentos'), $this->upload($key, true))->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('document_id', $id); $this->assertDatabaseHas('partnership_documents', ['id' => $link->id, 'shared' => false]); Queue::assertPushed(ScanDocumentForMalware::class, 1); $this->assertSame($events, DB::table('partnership_events')->count()); $this->postJson($this->url('documentos'), $this->upload((string) Str::uuid()))->assertConflict(); $this->actingAs($this->candidate)->getJson($this->url())->assertOk()->assertJsonCount(0, 'documents'); $this->postJson($this->url('documentos'), $this->upload($key, true))->assertConflict(); } public function test_message_recovery_after_closure_is_read_only_and_keys_are_not_disclosed_to_the_other_party(): void { $input = ['client_key' => (string) Str::uuid(), 'body' => 'SYNTHETIC uncertain message']; $this->postJson($this->url('mensagens'), $input)->assertOk(); $this->getJson($this->url())->assertOk()->assertJsonPath('messages.0.client_key', $input['client_key']); $this->actingAs($this->candidate)->getJson($this->url())->assertOk()->assertJsonPath('messages.0.client_key', null); $this->actingAs($this->owner); $this->closeRoom(); $events = DB::table('partnership_events')->count(); $this->postJson($this->url('mensagens'), $input)->assertOk(); $this->postJson($this->url('mensagens'), [...$input, 'body' => 'SYNTHETIC different content'])->assertConflict(); $this->postJson($this->url('mensagens'), [...$input, 'client_key' => (string) Str::uuid()])->assertConflict(); $this->assertDatabaseCount('partnership_messages', 1); $this->assertSame($events, DB::table('partnership_events')->count()); } public function test_stale_agreement_and_milestone_updates_preserve_the_current_state_and_evidence(): void { $input = $this->milestone(); $id = $this->postJson($this->url('marcos'), $input)->assertOk()->json('milestone_id'); $this->putJson($this->url('marcos/'.$id), [...$input, 'version' => 1, 'status' => 'completed', 'evidence' => 'SYNTHETIC frozen result'])->assertOk(); $this->putJson($this->url('marcos/'.$id), [...$input, 'version' => 1])->assertConflict(); $this->postJson($this->url('acordo'), ['version' => 1, 'action' => 'record', 'title' => 'SYNTHETIC stale', 'terms' => 'SYNTHETIC stale terms', 'reason' => 'SYNTHETIC stale attempt'])->assertConflict(); $this->assertDatabaseHas('partnership_milestones', ['id' => $id, 'version' => 2, 'status' => 'completed', 'evidence' => 'SYNTHETIC frozen result']); $this->assertDatabaseHas('partnership_rooms', ['id' => $this->roomId, 'version' => 2, 'status' => 'agreement_recorded']); } public function test_failed_document_event_rolls_back_the_new_file_without_removing_an_existing_original(): void { $id = $this->postJson($this->url('documentos'), $this->upload((string) Str::uuid()))->assertOk()->json('document_id'); $original = DB::table('documents')->where('id', $id)->first(); $exchange = \Mockery::mock(PartnershipExchange::class)->makePartial(); $exchange->shouldReceive('event')->once()->andThrow(new \RuntimeException('SYNTHETIC event failure')); $this->app->instance(PartnershipExchange::class, $exchange); $input = $this->upload((string) Str::uuid(), false, 'SYNTHETIC failed second upload'); try { app(PartnershipRoomUploads::class)->save($this->owner, $this->roomId, $input['file'], false, $input['client_key']); $this->fail('The synthetic event failure should abort the new upload.'); } catch (\RuntimeException $error) { $this->assertSame('SYNTHETIC event failure', $error->getMessage()); } $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('partnership_documents', 1); $this->assertCount(1, Storage::disk('local')->allFiles()); $this->assertSame($original->sha256, hash_file('sha256', Storage::disk('local')->path($original->storage_key))); Queue::assertPushed(ScanDocumentForMalware::class, 1); } }