Receber documentos privados e locais de disponibilidade nas candidaturas
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-30 20:52:10 +02:00
parent 2134aa084b
commit bf84575d10
26 changed files with 2756 additions and 49 deletions
+96 -3
View File
@@ -10,11 +10,15 @@ use App\Services\PeopleAccess;
use App\Services\PeopleHiring;
use App\Services\RequisitionWorkflow;
use App\Services\SubmissionForms;
use Illuminate\Database\QueryException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
beforeEach(function () {
Storage::fake('candidate_documents');
$this->tenant = Tenant::query()->create(['name' => 'Empresa fictícia de submissões']);
$this->admin = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Admin]);
$this->reviewer = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Admin]);
@@ -52,6 +56,17 @@ test('electronic submission returns a scoped receipt and preserves the actual pr
$this->postJson($url, [...$this->body, 'operation_key' => (string) Str::uuid(), 'profile_version' => 2])->assertConflict();
});
test('all availability locations travel with the submitted profile and remain unchanged after profile edits', function () {
$extra = ['province' => 'Outra província', 'district' => 'Outro distrito', 'locality' => 'Outra localidade'];
app(CandidateProfiles::class)->save($this->account, [...$this->profile, 'version' => 1, 'additional_availability' => [$extra]]);
$this->postJson($this->base.'/oportunidades/'.$this->process->id, [...$this->body, 'profile_version' => 2])->assertRedirect();
$row = DB::table('candidate_submissions')->sole();
app(CandidateProfiles::class)->save($this->account, [...$this->profile, 'version' => 2, 'additional_availability' => []]);
$this->get($this->base.'/submissoes/'.$row->id)->assertInertia(fn (Assert $p) => $p->where('receipt.snapshot.profile.data.additional_availability', [$extra]));
$this->get('/pessoas/'.$this->process->id.'/recebidas')->assertInertia(fn (Assert $p) => $p->where('submissions.data.0.snapshot.profile.data.additional_availability', [$extra]));
expect(DB::table('candidate_submissions')->sole()->snapshot)->toBe($row->snapshot);
});
test('withdrawn opportunities stale profiles and unverified contacts cannot submit', function () {
$url = $this->base.'/oportunidades/'.$this->process->id;
$this->postJson($url, [...$this->body, 'profile_version' => 99])->assertUnprocessable();
@@ -76,7 +91,7 @@ test('received submissions do not become internal evaluations and only the assig
$this->postJson('/pessoas/'.$this->process->id.'/publicacao', [...$this->publication, 'version' => 1, 'active' => false])->assertForbidden();
});
test('an opportunity requiring files cannot be published through the data only reception path', function () {
test('an opportunity requiring a CV can be published but cannot receive an incomplete submission', function () {
$this->actingAs($this->admin);
$definition = [...$this->definition, 'documents' => [['key' => 'cv', 'label' => 'CV', 'formats' => ['pdf'], 'max_kb' => 1024, 'required' => true]]];
app(SubmissionForms::class)->save($this->admin, ['audience' => 'service', 'modality' => 'competition', 'version' => 1, 'definition' => $definition, 'reason' => 'Exigir CV no novo formulário fictício.']);
@@ -84,8 +99,86 @@ test('an opportunity requiring files cannot be published through the data only r
app(PeopleAccess::class)->assign($this->admin, $record->id, ['version' => 1, 'user_id' => $this->reviewer->id, 'scopes' => ['review'], 'reason' => 'Conferência independente do novo processo.']);
$this->actingAs($this->reviewer);
app(PeopleHiring::class)->execute($this->reviewer, $record->id, 'open', ['version' => 1, 'reason' => 'Abertura fictícia para testar os anexos.', 'no_conflict' => true]);
$this->actingAs($this->admin)->postJson('/pessoas/'.$record->id.'/publicacao', $this->publication)->assertUnprocessable();
expect(DB::table('candidate_opportunity_publications')->where('people_process_id', $record->id)->count())->toBe(0);
$this->actingAs($this->admin)->postJson('/pessoas/'.$record->id.'/publicacao', $this->publication)->assertRedirect();
$this->postJson($this->base.'/oportunidades/'.$record->id, $this->body)->assertUnprocessable();
expect(DB::table('candidate_submissions')->where('people_process_id', $record->id)->count())->toBe(0);
});
function requireElectronicCv(object $test): void
{
$test->actingAs($test->admin);
$definition = [...$test->definition, 'documents' => [['key' => 'cv', 'label' => 'Currículo', 'formats' => ['pdf'], 'max_kb' => 1024, 'required' => true]]];
app(SubmissionForms::class)->save($test->admin, ['audience' => 'service', 'modality' => 'competition', 'version' => 1, 'definition' => $definition, 'reason' => 'Exigir currículo no formulário de teste.']);
$test->process = app(PeopleHiring::class)->execute($test->admin, null, 'create', ['operation_key' => (string) Str::uuid(), 'kind' => 'service', 'title' => 'Processo com currículo', 'purpose' => 'Conferência fictícia da recepção de documentos.', 'closes_on' => today()->addWeek()->toDateString(), 'minimum_reviews' => 1, 'criteria' => [['label' => 'Competências', 'weight' => 100]]]);
app(PeopleAccess::class)->assign($test->admin, $test->process->id, ['version' => 1, 'user_id' => $test->reviewer->id, 'scopes' => ['review'], 'reason' => 'Conferência independente do processo com CV.']);
$test->actingAs($test->reviewer);
app(PeopleHiring::class)->execute($test->reviewer, $test->process->id, 'open', ['version' => 1, 'reason' => 'Abrir processo fictício com documentos.', 'no_conflict' => true]);
$test->actingAs($test->admin);
app(CandidateSubmissions::class)->publish($test->admin, $test->process->id, $test->publication);
}
function electronicPdf(string $extra = ''): UploadedFile
{
return UploadedFile::fake()->createWithContent('curriculo.pdf', "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n".str_repeat(' ', 1024).$extra."\n%%EOF\n");
}
test('private CV receipt is immutable idempotent and downloadable only by its owner and authorised staff', function () {
requireElectronicCv($this);
$url = $this->base.'/oportunidades/'.$this->process->id;
$payload = [...$this->body, 'documents' => ['cv' => electronicPdf()]];
$this->post($url, $payload)->assertRedirect()->assertSessionHasNoErrors();
$row = DB::table('candidate_submissions')->sole();
$document = DB::table('candidate_submission_documents')->sole();
Storage::disk('candidate_documents')->assertExists($document->path);
$receipt = $this->base.'/submissoes/'.$row->id;
$this->get($receipt)->assertInertia(fn (Assert $p) => $p->where('receipt.snapshot.documents.cv.sha256', $document->sha256)->missing('receipt.snapshot.documents.cv.path'));
$this->get($receipt.'/documentos/cv')->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff')->assertDownload('curriculo.pdf');
$foreign = Tenant::query()->create(['name' => 'Empresa estrangeira fictícia']);
$this->get('/candidaturas/'.$foreign->id.'/submissoes/'.$row->id.'/documentos/cv')->assertUnauthorized();
$staffUrl = '/pessoas/'.$this->process->id.'/recebidas/'.$row->id.'/documentos/cv';
$this->get($staffUrl)->assertOk()->assertDownload('curriculo.pdf');
$this->post($url, [...$this->body, 'documents' => ['cv' => electronicPdf()]])->assertRedirect();
expect(DB::table('candidate_submission_documents')->count())->toBe(1)->and(Storage::disk('candidate_documents')->allFiles())->toHaveCount(1);
$this->post($url, [...$this->body, 'documents' => ['cv' => electronicPdf('changed')]])->assertConflict();
$this->actingAs($this->reviewer)->get($staffUrl)->assertForbidden();
$this->post($this->base.'/sair')->assertRedirect();
$this->get($receipt.'/documentos/cv')->assertUnauthorized();
$this->postJson($this->base.'/registar', [...$this->credentials, 'email' => 'other@example.invalid'])->assertRedirect();
$this->get($receipt.'/documentos/cv')->assertNotFound();
expect(DB::table('candidate_submissions')->sole()->snapshot)->toBe($row->snapshot);
});
test('missing forged unknown oversized and globally excessive files are rejected without stored files', function () {
requireElectronicCv($this);
$url = $this->base.'/oportunidades/'.$this->process->id;
foreach ([[], ['cv' => UploadedFile::fake()->createWithContent('fake.pdf', 'not a pdf')], ['unknown' => electronicPdf()], ['cv' => UploadedFile::fake()->create('large.pdf', 1025, 'application/pdf')]] as $documents) {
$this->postJson($url, [...$this->body, 'documents' => $documents])->assertUnprocessable();
}
config(['candidate_documents.max_total_kb' => 1]);
$this->postJson($url, [...$this->body, 'documents' => ['cv' => electronicPdf(str_repeat('x', 2048))]])->assertUnprocessable();
expect(DB::table('candidate_submissions')->count())->toBe(0)->and(Storage::disk('candidate_documents')->allFiles())->toBe([]);
});
test('failed audit removes uploaded bytes and rolls back the submission and document metadata', function () {
requireElectronicCv($this);
$this->partialMock(CandidateProfiles::class, fn ($mock) => $mock->shouldReceive('event')->once()->andThrow(new RuntimeException('Injected upload audit failure')));
$this->withoutExceptionHandling();
try {
$this->post($this->base.'/oportunidades/'.$this->process->id, [...$this->body, 'documents' => ['cv' => electronicPdf()]]);
$this->fail('Expected audit failure.');
} catch (RuntimeException $error) {
expect($error->getMessage())->toBe('Injected upload audit failure');
}
expect(DB::table('candidate_submissions')->count())->toBe(0)->and(DB::table('candidate_submission_documents')->count())->toBe(0)->and(Storage::disk('candidate_documents')->allFiles())->toBe([]);
});
test('download refuses changed stored bytes and document rows cannot be rewritten', function () {
requireElectronicCv($this);
$this->post($this->base.'/oportunidades/'.$this->process->id, [...$this->body, 'documents' => ['cv' => electronicPdf()]])->assertRedirect();
$document = DB::table('candidate_submission_documents')->sole();
Storage::disk('candidate_documents')->put($document->path, 'changed bytes');
$this->get($this->base.'/submissoes/'.$document->candidate_submission_id.'/documentos/cv')->assertConflict();
expect(fn () => DB::table('candidate_submission_documents')->where('id', $document->id)->update(['original_name' => 'other.pdf']))->toThrow(QueryException::class);
});
function electronicTriagePayload(object $test, string $outcome = 'accepted_for_analysis'): array