Files
compras/tests/Feature/PeopleRegistryTest.php
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

113 lines
9.0 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\PeopleProcess;
use App\Models\Tenant;
use App\Models\User;
use App\Services\PeopleAccess;
use App\Services\PeopleHiring;
use App\Services\RequisitionWorkflow;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use Inertia\Testing\AssertableInertia as Assert;
beforeEach(function () {
$this->tenant = Tenant::query()->create(['name' => 'Empresa fictícia de cadastro']);
$this->author = 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]);
$this->actingAs($this->author);
$this->personBody = ['operation_key' => (string) Str::uuid(), 'name' => 'Pessoa original fictícia', 'reference' => 'CAD-001',
'profile_summary' => 'Perfil profissional fictício registado uma vez.', 'reason' => 'Registo inicial fictício autorizado.'];
});
function registryOpenProcess(object $test, string $kind = 'service'): PeopleProcess
{
$key = (string) Str::uuid();
$test->actingAs($test->author)->postJson('/pessoas', ['operation_key' => $key, 'kind' => $kind, 'title' => 'Processo de reutilização fictício', 'purpose' => 'Necessidade fictícia independente do cadastro.',
'closes_on' => today()->toDateString(), 'minimum_reviews' => 1, 'criteria' => [['label' => 'Competências', 'weight' => 100]]])->assertRedirect();
$record = PeopleProcess::query()->where('operation_key', $key)->firstOrFail();
app(PeopleAccess::class)->assign($test->author, $record->id, ['version' => 1, 'user_id' => $test->reviewer->id, 'scopes' => ['review', 'interviews'], 'reason' => 'Conferente fictício do processo.']);
$test->actingAs($test->reviewer)->postJson('/pessoas/'.$record->id.'/open', ['version' => 1, 'reason' => 'Abertura conferida de forma independente.', 'no_conflict' => true])->assertRedirect();
return $record->fresh();
}
function registryApply(object $test, PeopleProcess $record, array $input): TestResponse
{
return $test->actingAs($test->author)->postJson('/pessoas/'.$record->id.'/apply', ['version' => $record->fresh()->version,
'operation_key' => (string) Str::uuid(), 'reference' => 'DOC-'.Str::uuid(), 'received_on' => today()->toDateString(), 'reason' => 'Registo fictício de candidatura.', ...$input]);
}
test('one person is reused across both hiring paths with frozen source versions and no duplicate candidature', function (string $kind) {
$this->postJson('/pessoas/cadastro', $this->personBody)->assertRedirect();
$person = DB::table('people_registry')->sole();
$first = registryOpenProcess($this, $kind);
$this->actingAs($this->author)->get('/pessoas/'.$first->id.'?cadastro=CAD-001')->assertInertia(fn (Assert $p) => $p->has('registry.items', 1)->where('registry.items.0.id', $person->id));
registryApply($this, $first, ['person_id' => $person->id, 'person_version' => 1, 'name' => 'NOME FORJADO IGNORADO'])->assertRedirect();
$original = DB::table('people_applications')->sole();
expect($original->name)->toBe($this->personBody['name'])->and(json_decode($original->person_snapshot, true)['version'])->toBe(1);
$this->postJson('/pessoas/cadastro/'.$person->id, [...$this->personBody, 'version' => 1, 'active' => true, 'name' => 'Nome actualizado fictício', 'profile_summary' => 'Perfil actualizado para novos processos.'])->assertRedirect();
expect(app(PeopleHiring::class)->matrix($first)[0]['name'])->toBe($original->name)
->and(app(PeopleHiring::class)->matrix($first)[0]['registry_source']['version'])->toBe(1);
registryApply($this, $first, ['person_id' => $person->id, 'person_version' => 2])->assertConflict();
$second = registryOpenProcess($this, $kind === 'service' ? 'staff' : 'service');
registryApply($this, $second, ['person_id' => $person->id, 'person_version' => 1])->assertUnprocessable();
registryApply($this, $second, ['person_id' => $person->id, 'person_version' => 2, 'summary' => 'Complemento específico desta candidatura.'])->assertRedirect();
$new = DB::table('people_applications')->where('people_process_id', $second->id)->sole();
expect($new->name)->toBe('Nome actualizado fictício')->and($new->summary)->toBe('Complemento específico desta candidatura.')
->and(json_decode($new->person_snapshot, true)['version'])->toBe(2)
->and(DB::table('people_applications')->where('id', $original->id)->first()->person_snapshot)->toBe($original->person_snapshot);
$this->postJson('/pessoas/cadastro/'.$person->id, [...$this->personBody, 'version' => 2, 'active' => false])->assertRedirect();
$third = registryOpenProcess($this);
registryApply($this, $third, ['person_id' => $person->id, 'person_version' => 3])->assertUnprocessable();
expect(fn () => DB::transaction(fn () => DB::table('people_applications')->where('id', $original->id)->update(['person_snapshot' => '{}'])))->toThrow(QueryException::class);
})->with(['service', 'staff']);
test('first registration can save a private reusable profile without entering name and profile again', function () {
$record = registryOpenProcess($this);
registryApply($this, $record, ['name' => 'Pessoa registada no fluxo', 'summary' => 'Experiência profissional fictícia reutilizável.', 'save_person' => true])->assertRedirect();
$person = DB::table('people_registry')->sole();
expect($person->name)->toBe('Pessoa registada no fluxo')->and($person->profile_summary)->toBe('Experiência profissional fictícia reutilizável.')
->and(DB::table('people_applications')->sole()->person_id)->toBe($person->id);
$this->actingAs($this->reviewer)->get('/pessoas/cadastro')->assertInertia(fn (Assert $p) => $p->where('records.total', 0));
$this->postJson('/pessoas/cadastro/'.$person->id, [...$this->personBody, 'version' => 1, 'active' => true])->assertNotFound();
});
test('shared directory permissions are explicit and never grant access to a persons processes', function () {
$this->postJson('/pessoas/cadastro', $this->personBody)->assertRedirect();
$record = registryOpenProcess($this);
$reader = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Configured]);
$profile = DB::table('organization_profiles')->insertGetId(['tenant_id' => $this->tenant->id, 'name' => 'Consulta de cadastro', 'permissions' => json_encode(['people_registry_read']), 'version' => 1]);
DB::table('organization_memberships')->insert(['tenant_id' => $this->tenant->id, 'user_id' => $reader->id, 'profile_id' => $profile, 'maintenance' => false]);
DB::table('organization_access')->insert(['tenant_id' => $this->tenant->id, 'active' => true, 'version' => 1]);
$this->actingAs($reader)->get('/pessoas/cadastro')->assertInertia(fn (Assert $p) => $p->where('records.total', 1)->where('records.data.0.canEdit', false));
$this->get('/pessoas/'.$record->id)->assertForbidden();
$this->postJson('/pessoas/cadastro', [...$this->personBody, 'operation_key' => (string) Str::uuid()])->assertForbidden();
DB::table('organization_memberships')->where('user_id', $reader->id)->update(['expires_at' => now()->subMinute()]);
$this->get('/pessoas/cadastro')->assertForbidden();
});
test('registry references are unique per company without merging names and foreign company records cannot be reused', function () {
$this->postJson('/pessoas/cadastro', $this->personBody)->assertRedirect();
$this->postJson('/pessoas/cadastro', $this->personBody)->assertRedirect();
expect(DB::table('people_registry')->count())->toBe(1);
$this->postJson('/pessoas/cadastro', [...$this->personBody, 'operation_key' => (string) Str::uuid(), 'reference' => ' cad-001 '])->assertUnprocessable();
$foreign = Tenant::query()->create(['name' => 'Outra empresa fictícia']);
$user = User::factory()->create(['tenant_id' => $foreign->id, 'purchasing_role' => PurchasingRole::Admin]);
$this->actingAs($user)->postJson('/pessoas/cadastro', [...$this->personBody, 'operation_key' => (string) Str::uuid()])->assertRedirect();
$other = DB::table('people_registry')->where('tenant_id', $foreign->id)->sole();
$record = registryOpenProcess($this);
registryApply($this, $record, ['person_id' => $other->id, 'person_version' => 1])->assertNotFound();
});
test('audit failure rolls back the first registry record candidature and process version together', function () {
$record = registryOpenProcess($this);
$version = $record->version;
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected registry audit failure'));
$this->withoutExceptionHandling();
expect(fn () => registryApply($this, $record, ['save_person' => true, 'name' => 'Pessoa fictícia', 'summary' => 'Perfil fictício que não deve persistir.']))->toThrow(RuntimeException::class);
expect(DB::table('people_registry')->count())->toBe(0)->and(DB::table('people_applications')->count())->toBe(0)->and($record->fresh()->version)->toBe($version);
});