Files
compras/tests/Feature/CandidatePortalTest.php
2026-10-30 20:52:10 +02:00

112 lines
8.2 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\CandidateAccount;
use App\Models\User;
use App\Services\CandidateProfiles;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
beforeEach(function () {
$this->f = receiptFixture();
$this->admin = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['department']->id, 'purchasing_role' => PurchasingRole::Admin]);
$this->actingAs($this->admin);
$this->base = '/candidaturas/'.$this->f['tenant']->id;
$this->settings = ['version' => 0, 'registration_open' => true, 'notice' => 'Aviso fictício para testar o perfil, sem utilização de dados reais.', 'reason' => 'Configuração fictícia da área do candidato.'];
$this->registration = ['email' => 'candidate@example.invalid', 'password' => 'Candidate-Test-123!', 'password_confirmation' => 'Candidate-Test-123!', 'notice_acknowledged' => true, 'portal_version' => 1];
$this->profile = ['version' => 0, 'name' => 'Pessoa fictícia', 'phone' => '000000', 'summary' => 'Resumo fictício de competências.',
'availability' => ['province' => 'Província fictícia', 'district' => 'Distrito fictício', 'locality' => 'Localidade fictícia'], 'reason' => 'Primeiro preenchimento fictício.'];
});
test('portal starts unavailable and only company administrators configure it', function () {
$this->get($this->base)->assertNotFound();
$this->actingAs($this->f['receiver'])->postJson('/administracao/candidaturas/portal', $this->settings)->assertForbidden();
$this->actingAs($this->admin)->postJson('/administracao/candidaturas/portal', $this->settings)->assertRedirect();
$this->postJson('/administracao/candidaturas/portal', $this->settings)->assertUnprocessable();
$this->get($this->base)->assertInertia(fn (Assert $p) => $p->where('portal.registration_open', true)->where('auth.user', null)->where('organizationAccess', null));
});
test('candidate account is separate from staff and profile versions preserve geography without granting internal access', function () {
app(CandidateProfiles::class)->configure($this->admin, $this->settings);
Auth::guard('web')->logout();
$this->postJson($this->base.'/registar', $this->registration)->assertRedirect($this->base.'/perfil');
$account = CandidateAccount::query()->sole();
expect(Hash::check($this->registration['password'], $account->password))->toBeTrue();
$this->assertAuthenticatedAs($account, 'candidate');
$this->assertGuest('web');
$this->get('/pessoas')->assertRedirect();
$this->postJson($this->base.'/perfil', $this->profile)->assertRedirect();
$this->postJson($this->base.'/perfil', $this->profile)->assertUnprocessable();
$this->postJson($this->base.'/perfil', [...$this->profile, 'version' => 1, 'name' => 'Pessoa fictícia actualizada'])->assertRedirect();
$this->get($this->base.'/perfil')->assertInertia(fn (Assert $p) => $p->where('profile.version', 2)->where('profile.data.name', 'Pessoa fictícia actualizada')->where('profile.data.availability.district', 'Distrito fictício')->where('auth.user', null)->missing('profile.data.password')->missing('reviews'));
$first = DB::table('candidate_profile_versions')->orderBy('version')->first();
expect(json_decode($first->data, true)['name'])->toBe('Pessoa fictícia');
$this->post($this->base.'/sair')->assertRedirect($this->base);
$this->assertGuest('candidate');
$this->get($this->base.'/perfil')->assertRedirect($this->base);
});
test('closed registration preserves existing access and stale notices cannot create accounts', function () {
app(CandidateProfiles::class)->configure($this->admin, $this->settings);
$this->postJson($this->base.'/registar', [...$this->registration, 'portal_version' => 2])->assertUnprocessable();
$this->postJson($this->base.'/registar', $this->registration)->assertRedirect();
app(CandidateProfiles::class)->configure($this->admin, [...$this->settings, 'version' => 1, 'registration_open' => false]);
$this->post($this->base.'/sair')->assertRedirect();
$this->postJson($this->base.'/registar', [...$this->registration, 'email' => 'other@example.invalid', 'portal_version' => 2])->assertForbidden();
$this->postJson($this->base.'/entrar', $this->registration)->assertRedirect();
$this->get($this->base.'/perfil')->assertOk();
$this->assertAuthenticatedAs($this->admin, 'web');
});
test('one candidate never reads another company profile or another account profile', function () {
app(CandidateProfiles::class)->configure($this->admin, $this->settings);
$this->postJson($this->base.'/registar', $this->registration)->assertRedirect();
$first = CandidateAccount::query()->sole();
$this->postJson($this->base.'/perfil', $this->profile)->assertRedirect();
$foreign = receiptFixture();
$this->postJson('/candidaturas/'.$foreign['tenant']->id.'/perfil', $this->profile)->assertUnauthorized();
$this->get('/candidaturas/'.$foreign['tenant']->id.'/perfil')->assertRedirect();
$this->post($this->base.'/sair')->assertRedirect();
$this->postJson($this->base.'/registar', [...$this->registration, 'email' => 'another@example.invalid'])->assertRedirect();
$this->get($this->base.'/perfil')->assertInertia(fn (Assert $p) => $p->where('profile.version', 0)->where('profile.data.name', ''));
$this->postJson($this->base.'/perfil', [...$this->profile, 'candidate_account_id' => $first->id])->assertRedirect();
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $first->id)->count())->toBe(1);
});
test('multiple availability locations are versioned and legacy requests preserve them', function () {
app(CandidateProfiles::class)->configure($this->admin, $this->settings);
$this->postJson($this->base.'/registar', $this->registration)->assertRedirect();
$extra = ['province' => 'Outra província', 'district' => 'Outro distrito', 'locality' => 'Outra localidade'];
$this->postJson($this->base.'/perfil', [...$this->profile, 'additional_availability' => [$extra]])->assertRedirect();
$this->postJson($this->base.'/perfil', [...$this->profile, 'version' => 1])->assertRedirect();
$this->get($this->base.'/perfil')->assertInertia(fn (Assert $p) => $p->where('profile.data.additional_availability', [$extra]));
$this->postJson($this->base.'/perfil', [...$this->profile, 'version' => 2, 'additional_availability' => []])->assertRedirect();
$versions = DB::table('candidate_profile_versions')->orderBy('version')->get();
expect(json_decode($versions[0]->data, true)['additional_availability'])->toBe([$extra])
->and(json_decode($versions[2]->data, true)['additional_availability'])->toBe([]);
});
test('additional locations reject incomplete duplicate and excessive entries without writing a version', function () {
app(CandidateProfiles::class)->configure($this->admin, $this->settings);
$this->postJson($this->base.'/registar', $this->registration)->assertRedirect();
$duplicate = array_map(fn ($value) => ' '.mb_strtoupper($value).' ', $this->profile['availability']);
foreach ([[$duplicate], [['province' => 'Parcial']], array_fill(0, 21, $this->profile['availability'])] as $locations) {
$this->postJson($this->base.'/perfil', [...$this->profile, 'additional_availability' => $locations])->assertUnprocessable();
}
expect(DB::table('candidate_profile_versions')->count())->toBe(0);
});
test('profile and event writes roll back together and missing geography is rejected', function () {
app(CandidateProfiles::class)->configure($this->admin, $this->settings);
$this->postJson($this->base.'/registar', $this->registration)->assertRedirect();
$this->postJson($this->base.'/perfil', [...$this->profile, 'availability' => ['province' => 'Parcial']])->assertUnprocessable();
$this->partialMock(CandidateProfiles::class, fn ($mock) => $mock->shouldReceive('event')->once()->andThrow(new RuntimeException('Injected candidate audit failure')));
$this->withoutExceptionHandling();
expect(fn () => $this->postJson($this->base.'/perfil', $this->profile))->toThrow(RuntimeException::class);
expect(DB::table('candidate_profile_versions')->count())->toBe(0);
});