Files
compras/tests/Feature/CandidateGeographyTest.php
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

180 lines
15 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\CandidateAccount;
use App\Models\User;
use App\Services\AdministrativeGeography;
use App\Services\CandidateProfiles;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
function additionalGeographyReference(int $index = 0): array
{
$record = collect(app(AdministrativeGeography::class)->catalogue()['records'])->filter(fn ($record) => $record['level'] === 4 && $record['selectable'] && $record['id'] !== '5')->values()->get($index);
return ['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => $record['id']];
}
beforeEach(function () {
$this->withoutVite();
config(['geography.enabled' => true]);
$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);
app(CandidateProfiles::class)->configure($this->admin, ['version' => 0, 'registration_open' => true, 'notice' => 'Aviso sintético de testes, sem dados ou contas reais.', 'reason' => 'Configuração de teste da geografia.']);
$this->base = '/candidaturas/'.$this->f['tenant']->id;
$this->post($this->base.'/registar', ['email' => 'geo-candidate@example.test', 'password' => 'Synthetic-Geo-123!', 'password_confirmation' => 'Synthetic-Geo-123!', 'notice_acknowledged' => true, 'portal_version' => 1])->assertRedirect();
$this->account = CandidateAccount::query()->sole();
$this->reference = ['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => '5'];
$this->profileData = ['version' => 0, 'name' => 'Pessoa sintética', 'phone' => '', 'summary' => 'Perfil de competências de teste.', 'availability' => ['province' => 'Texto original', 'district' => 'Distrito original', 'locality' => 'Localidade original'], 'reason' => 'Registo sintético de disponibilidade.'];
});
test('candidate geography catalog is scoped to a current candidate session without staff grants', function () {
Auth::guard('web')->logout();
$this->getJson($this->base.'/divisao-administrativa')->assertOk()->assertJsonCount(11, 'data')->assertHeader('Cache-Control', 'max-age=0, no-store, private');
$this->getJson($this->base.'/divisao-administrativa?parent=2')->assertOk();
$this->getJson('/api/divisao-administrativa')->assertUnauthorized();
$this->getJson('/candidaturas/999999/divisao-administrativa')->assertUnauthorized();
$this->account->increment('session_version');
$this->getJson($this->base.'/divisao-administrativa')->assertUnauthorized();
});
test('candidate geography endpoint rejects guests and staff identities without candidate authentication', function () {
Auth::guard('candidate')->logout();
$this->getJson($this->base.'/divisao-administrativa')->assertUnauthorized();
Auth::guard('web')->logout();
$this->getJson($this->base.'/divisao-administrativa')->assertUnauthorized();
});
test('candidate geography derives official availability from the reference and preserves earlier manual versions', function () {
$this->post($this->base.'/perfil', $this->profileData)->assertSessionHasNoErrors();
$first = DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->first()->data;
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1, 'availability_reference' => $this->reference])->assertSessionHasNoErrors();
$profile = app(CandidateProfiles::class)->profile($this->account);
expect($profile['data']['availability_reference'])->toBe($this->reference)->and($profile['data']['availability'])->toBe(['province' => 'Niassa', 'district' => 'CIDADE DE LICHINGA', 'locality' => 'Urbana nº 1']);
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->orderBy('version')->first()->data)->toBe($first);
$this->get($this->base.'/perfil')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', true)->where('availabilityGeography.reference.record_id', '5')->where('auth.user', null));
});
test('candidate geography legacy updates preserve known references only while availability text is unchanged', function () {
$this->post($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $this->reference])->assertSessionHasNoErrors();
$current = app(CandidateProfiles::class)->profile($this->account);
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1, 'availability' => array_reverse($current['data']['availability'], true)])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data']['availability_reference'])->toBe($this->reference);
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 2])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data'])->not->toHaveKey('availability_reference');
});
test('candidate geography rejects ambiguous incomplete and obsolete references without creating profile versions', function () {
$bad = collect(app(AdministrativeGeography::class)->catalogue()['records'])->first(fn ($record) => in_array('duplicate_code_path', $record['issues'], true));
foreach ([['record_id' => '999999'], ['record_id' => $bad['id']], ['record_id' => '2'], ['catalogue' => str_repeat('0', 64)]] as $change) {
$this->postJson($this->base.'/perfil', [...$this->profileData, 'availability_reference' => [...$this->reference, ...$change]])->assertUnprocessable();
}
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->count())->toBe(0);
});
test('candidate geography keeps native profile version and duplicate availability rules', function () {
$this->post($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $this->reference])->assertSessionHasNoErrors();
$this->postJson($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $this->reference])->assertUnprocessable()->assertJsonValidationErrors('version');
$location = app(CandidateProfiles::class)->profile($this->account)['data']['availability'];
$this->postJson($this->base.'/perfil', [...$this->profileData, 'version' => 1, 'availability_reference' => $this->reference, 'additional_availability' => [$location]])->assertUnprocessable()->assertJsonValidationErrors('additional_availability');
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->count())->toBe(1);
});
test('candidate geography can be cleared explicitly while foreign and unresolved locations stay manual', function () {
$this->post($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $this->reference])->assertSessionHasNoErrors();
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1, 'availability_reference' => null])->assertSessionHasNoErrors();
$profile = app(CandidateProfiles::class)->profile($this->account);
expect($profile['data']['availability_reference'])->toBeNull()->and($profile['data']['availability'])->toBe($this->profileData['availability']);
});
test('candidate geography disabled keeps manual profiles and historical references usable', function () {
$this->post($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $this->reference])->assertSessionHasNoErrors();
$location = app(CandidateProfiles::class)->profile($this->account)['data']['availability'];
config(['geography.enabled' => false]);
$this->getJson($this->base.'/divisao-administrativa')->assertServiceUnavailable();
$this->get($this->base.'/perfil')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', false));
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1, 'availability' => $location])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data']['availability_reference'])->toBe($this->reference);
});
test('candidate geography event failure rolls back profile and resolved reference together', function () {
$this->partialMock(CandidateProfiles::class)->shouldReceive('event')->andThrow(new RuntimeException('Synthetic candidate event failure'));
$this->withoutExceptionHandling();
expect(fn () => $this->post($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $this->reference]))->toThrow(RuntimeException::class, 'Synthetic candidate event failure');
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->count())->toBe(0);
});
test('candidate additional geography resolves each selected locality and preserves manual entries', function () {
$reference = additionalGeographyReference();
$manual = $this->profileData['availability'];
$manual['locality'] = 'Outro local manual';
$this->post($this->base.'/perfil', [...$this->profileData, 'additional_availability' => [$manual, []], 'additional_availability_references' => [null, $reference]])->assertSessionHasNoErrors();
$data = app(CandidateProfiles::class)->profile($this->account)['data'];
expect($data['additional_availability_references'])->toBe([null, $reference])->and($data['additional_availability'][0])->toBe($manual);
$path = app(AdministrativeGeography::class)->describe($reference)['path'];
$names = array_column($path, 'name', 'level');
expect($data['additional_availability'][1])->toBe(['province' => $names[1], 'district' => $names[2], 'locality' => $names[4]]);
});
test('candidate additional geography validates alignment list shape limits and invalid references', function () {
$entry = $this->profileData['availability'];
foreach ([
['additional_availability' => [$entry], 'additional_availability_references' => []],
['additional_availability' => [$entry], 'additional_availability_references' => [1 => additionalGeographyReference()]],
['additional_availability' => [$entry], 'additional_availability_references' => [[...$this->reference, 'record_id' => '999999']]],
['additional_availability' => array_fill(0, 21, $entry), 'additional_availability_references' => array_fill(0, 21, null)],
['additional_availability_references' => []],
] as $case) {
$this->postJson($this->base.'/perfil', [...$this->profileData, ...$case])->assertUnprocessable();
}
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->count())->toBe(0);
});
test('candidate additional geography legacy reorder removal and edits retain only known unchanged associations', function () {
$references = [additionalGeographyReference(), additionalGeographyReference(1)];
$this->post($this->base.'/perfil', [...$this->profileData, 'additional_availability' => [[], []], 'additional_availability_references' => $references])->assertSessionHasNoErrors();
$first = app(CandidateProfiles::class)->profile($this->account)['data'];
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1, 'additional_availability' => array_reverse($first['additional_availability'])])->assertSessionHasNoErrors();
$second = app(CandidateProfiles::class)->profile($this->account)['data'];
expect($second['additional_availability_references'])->toBe(array_reverse($references));
$edited = $second['additional_availability'];
$edited[0]['locality'] = 'Modificação manual sem associação';
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 2, 'additional_availability' => $edited])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data']['additional_availability_references'])->toBe([null, $references[0]]);
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 3, 'additional_availability' => []])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data']['additional_availability_references'])->toBe([]);
$old = json_decode(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->orderBy('version')->first()->data, true);
expect($old['additional_availability_references'])->toBe($references);
});
test('candidate additional geography omitted data stays intact while explicit null associations become manual', function () {
$reference = additionalGeographyReference();
$this->post($this->base.'/perfil', [...$this->profileData, 'additional_availability' => [[]], 'additional_availability_references' => [$reference]])->assertSessionHasNoErrors();
$first = app(CandidateProfiles::class)->profile($this->account)['data'];
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1])->assertSessionHasNoErrors();
$second = app(CandidateProfiles::class)->profile($this->account)['data'];
expect($second['additional_availability'])->toBe($first['additional_availability'])->and($second['additional_availability_references'])->toBe([$reference]);
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 2, 'additional_availability' => $second['additional_availability'], 'additional_availability_references' => [null]])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data']['additional_availability_references'])->toBe([null]);
});
test('candidate additional geography keeps duplicate and obsolete catalogue checks across all locations', function () {
$reference = additionalGeographyReference();
$this->postJson($this->base.'/perfil', [...$this->profileData, 'availability_reference' => $reference, 'additional_availability' => [[]], 'additional_availability_references' => [$reference]])->assertUnprocessable()->assertJsonValidationErrors('additional_availability');
$this->postJson($this->base.'/perfil', [...$this->profileData, 'additional_availability' => [[], []], 'additional_availability_references' => [$reference, $reference]])->assertUnprocessable()->assertJsonValidationErrors('additional_availability');
$this->postJson($this->base.'/perfil', [...$this->profileData, 'additional_availability' => [[]], 'additional_availability_references' => [[...$reference, 'catalogue' => str_repeat('0', 64)]]])->assertUnprocessable()->assertJsonValidationErrors('additional_availability_references.0');
expect(DB::table('candidate_profile_versions')->where('candidate_account_id', $this->account->id)->count())->toBe(0);
});
test('candidate additional geography suspended catalogue preserves existing associations through legacy requests', function () {
$reference = additionalGeographyReference();
$this->post($this->base.'/perfil', [...$this->profileData, 'additional_availability' => [[]], 'additional_availability_references' => [$reference]])->assertSessionHasNoErrors();
config(['geography.enabled' => false]);
$this->post($this->base.'/perfil', [...$this->profileData, 'version' => 1])->assertSessionHasNoErrors();
expect(app(CandidateProfiles::class)->profile($this->account)['data']['additional_availability_references'])->toBe([$reference]);
});