Files
Investidor-V2/tests/Feature/DeskDocumentReuseTest.php
T

167 lines
11 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
final class DeskDocumentReuseTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private string $caseId;
protected function setUp(): void
{
parent::setUp();
$this->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();
}
}