withoutVite(); Storage::fake('local'); Queue::fake(); $this->owner = User::factory()->create(); $this->actingAs($this->owner); $this->caseId = $this->makeCase('SYNTHETIC destination'); } private function makeCase(string $title): string { return $this->postJson('/balcao-virtual/dossiers', ['title' => $title, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); } private function upload(string $case, string $name = 'synthetic-source.txt'): array { return $this->post('/balcao-virtual/dossiers/'.$case.'/documentos', ['version' => DB::table('desk_cases')->where('id', $case)->value('version'), 'file' => UploadedFile::fake()->createWithContent($name, 'SYNTHETIC '.$name), 'category' => 'project_evidence', 'source_reference' => 'SYNTHETIC source '.$name, 'consent' => true], ['Accept' => 'application/json'])->assertOk()->json(); } private function catalogue(string $query = ''): TestResponse { return $this->getJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos-disponiveis'.$query); } public function test_personal_source_is_searchable_and_reused_without_reentering_reference_dates_or_copying_original(): void { $source = $this->makeCase('SYNTHETIC previous dossier'); $upload = $this->upload($source); DB::table('desk_documents')->where('id', $upload['link_id'])->update(['stage' => 'E2', 'valid_until' => '2028-02-03']); $this->catalogue('?q=previous')->assertOk()->assertJsonCount(1, 'documents')->assertJsonPath('documents.0.id', $upload['link_id'])->assertJsonPath('documents.0.valid_until', '2028-02-03')->assertHeader('Cache-Control', 'no-store, private'); $document = $upload['documents'][0]['document_id']; $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'document_id' => $document, 'reuse_link_id' => $upload['link_id'], 'category' => 'resource_evidence', 'consent' => true])->assertOk(); $this->assertDatabaseCount('documents', 1); $this->assertDatabaseCount('desk_documents', 2); $this->assertCount(1, Storage::disk('local')->allFiles()); $this->assertDatabaseHas('desk_documents', ['case_id' => $this->caseId, 'document_id' => $document, 'source_reference' => 'SYNTHETIC source synthetic-source.txt', 'stage' => 'E2', 'valid_until' => '2028-02-03']); $this->assertDatabaseHas('desk_cases', ['id' => $source, 'version' => 2]); } public function test_catalogue_matches_existing_authority_and_hides_another_dossiers_private_source_from_its_uploader(): void { $source = $this->makeCase('SYNTHETIC secret organization context'); $upload = $this->upload($source); $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC organization', 'organization_type' => 'private']); DB::table('desk_cases')->where('id', $source)->update(['organization_id' => $org]); $this->catalogue()->assertOk()->assertJsonCount(1, 'documents')->assertJsonPath('documents.0.id', null)->assertJsonPath('documents.0.source_reference', null)->assertJsonPath('documents.0.origin_label', null); $this->catalogue('?q=secret')->assertOk()->assertJsonCount(0, 'documents'); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'document_id' => $upload['documents'][0]['document_id'], 'reuse_link_id' => $upload['link_id'], 'category' => 'project_evidence', 'consent' => true])->assertForbidden(); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 1, 'document_id' => $upload['documents'][0]['document_id'], 'category' => 'project_evidence', 'consent' => true])->assertUnprocessable()->assertJsonValidationErrors('source_reference'); $other = User::factory()->create(); config(['portal.managers' => [$other->email]]); $this->actingAs($other); $this->catalogue()->assertForbidden(); } public function test_current_organization_files_are_reusable_but_revoked_members_cannot_keep_using_the_catalogue(): void { $upload = $this->upload($this->caseId); $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC collaboration', 'organization_type' => 'private']); DB::table('desk_cases')->where('id', $this->caseId)->update(['organization_id' => $org]); $colleague = User::factory()->create(); $membership = (string) Str::uuid(); DB::table('memberships')->insert(['id' => $membership, 'organization_id' => $org, 'user_id' => $colleague->id, 'role' => 'officer']); $this->actingAs($colleague); $this->catalogue()->assertOk()->assertJsonPath('documents.0.id', $upload['link_id']); DB::table('memberships')->where('id', $membership)->update(['role' => 'viewer']); $this->catalogue()->assertForbidden(); DB::table('memberships')->where('id', $membership)->update(['role' => 'officer', 'deleted_at' => now()]); $this->catalogue()->assertForbidden(); } public function test_unlinked_uploads_are_available_without_invented_provenance_and_foreign_or_deleted_uploads_are_absent(): void { $upload = $this->upload($this->caseId); DB::table('desk_documents')->where('id', $upload['link_id'])->delete(); $this->catalogue()->assertOk()->assertJsonCount(1, 'documents')->assertJsonPath('documents.0.id', null)->assertJsonPath('documents.0.source_reference', null); $document = $upload['documents'][0]['document_id']; $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'document_id' => $document, 'category' => 'project_evidence', 'source_reference' => 'SYNTHETIC supplied missing reference', 'consent' => true])->assertOk(); DB::table('documents')->where('id', $document)->update(['deleted_at' => now()]); $this->catalogue()->assertOk()->assertJsonCount(0, 'documents'); $this->actingAs(User::factory()->create()); $foreign = $this->makeCase('SYNTHETIC foreign'); $this->upload($foreign, 'private-foreign.txt'); $this->actingAs($this->owner); $this->catalogue()->assertOk()->assertJsonCount(0, 'documents'); } public function test_opportunity_source_authority_is_checked_in_catalogue_and_attachment_even_for_uploader(): void { $upload = $this->upload($this->caseId); $document = $upload['documents'][0]['document_id']; DB::table('documents')->where('id', $document)->update(['source_system' => 'opportunity-resources']); $this->catalogue()->assertOk()->assertJsonCount(0, 'documents'); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/documentos', ['version' => 2, 'document_id' => $document, 'category' => 'project_evidence', 'consent' => true])->assertForbidden(); $this->assertDatabaseHas('desk_cases', ['id' => $this->caseId, 'version' => 2]); } public function test_literal_search_pagination_and_invalid_input_do_not_expand_scope(): void { $upload = $this->upload($this->caseId, 'SYNTHETIC %_ literal.txt'); $row = (array) DB::table('documents')->where('id', $upload['documents'][0]['document_id'])->first(); for ($i = 0; $i < 27; $i++) { $copy = (string) Str::uuid(); $key = 'synthetic-catalogue/'.$copy; Storage::disk('local')->put($key, Storage::disk('local')->get($row['storage_key'])); DB::table('documents')->insert([...$row, 'id' => $copy, 'storage_key' => $key, 'file_name' => sprintf('SYNTHETIC paged %02d.txt', $i)]); } $this->catalogue('?q='.urlencode('%_'))->assertOk()->assertJsonCount(1, 'documents')->assertJsonPath('documents.0.file_name', 'SYNTHETIC %_ literal.txt'); $first = $this->catalogue('?q=paged')->assertOk()->assertJsonCount(25, 'documents')->assertJsonPath('next_offset', 25)->json('documents'); $last = $this->catalogue('?q=paged&offset=25')->assertOk()->assertJsonCount(2, 'documents')->assertJsonPath('next_offset', null)->json('documents'); $this->assertCount(27, array_unique(array_column([...$first, ...$last], 'document_id'))); $this->catalogue('?offset=-1')->assertUnprocessable(); $this->catalogue('?q='.str_repeat('x', 121))->assertUnprocessable(); } public function test_selected_stage_link_preserves_its_exact_provenance_and_legacy_document_id_still_works(): void { $upload = $this->upload($this->caseId); $other = $this->makeCase('SYNTHETIC other source'); $foreign = $this->upload($other, 'foreign-link.txt'); $body = ['version' => 2, 'stage' => 'E0', 'status' => 'preparing', 'reason' => 'SYNTHETIC exact support', 'document_link_id' => $upload['link_id']]; $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/etapas', [...$body, 'document_link_id' => $foreign['link_id']])->assertUnprocessable(); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/etapas', $body)->assertOk(); $case = DB::table('desk_cases')->where('id', $this->caseId)->first(); $snapshot = json_decode($case->workflow, true)['E0']; $this->assertSame($upload['link_id'], $snapshot['document_link_id']); $this->assertSame($upload['documents'][0]['document_id'], $snapshot['document_id']); $this->assertSame('SYNTHETIC source synthetic-source.txt', $snapshot['document_source']['source_reference']); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/etapas', ['version' => 3, 'stage' => 'E0', 'status' => 'preparing', 'reason' => 'SYNTHETIC legacy client', 'document_id' => $upload['documents'][0]['document_id']])->assertOk(); } public function test_closed_dossiers_remain_readable_without_upload_or_catalogue_and_anonymous_access_is_refused(): void { DB::table('desk_cases')->where('id', $this->caseId)->update(['closed_at' => now()]); $this->getJson('/balcao-virtual/dossiers/'.$this->caseId)->assertOk()->assertJsonPath('canWrite', false); $this->catalogue()->assertConflict(); auth()->logout(); $this->catalogue()->assertUnauthorized(); } }