Files
compras/tests/Feature/OrganizationConfigurationTest.php
T
nala 48347e2d3a
tests / ci (push) Canceled after 0s
commit5
2026-10-30 10:55:18 +02:00

165 lines
14 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\AuditLog;
use App\Models\User;
use App\Services\OrganizationAccess;
use App\Services\RequisitionWorkflow;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
function organizationProfile(int $tenant, string $name, array $permissions): int
{
return DB::table('organization_profiles')->insertGetId(['tenant_id' => $tenant, 'name' => $name, 'permissions' => json_encode($permissions, JSON_THROW_ON_ERROR), 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
}
function organizationMember(int $tenant, int $user, int $profile): void
{
DB::table('organization_memberships')->insert(['tenant_id' => $tenant, 'user_id' => $user, 'profile_id' => $profile, 'maintenance' => false, 'created_at' => now(), 'updated_at' => now()]);
}
beforeEach(function () {
$this->f = receiptFixture();
$this->admin = $this->f['issuer'];
$this->second = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['r']->department_id, 'purchasing_role' => PurchasingRole::Requester]);
$this->adminProfile = organizationProfile($this->f['tenant']->id, 'Direcção de sistemas', ['admin']);
organizationMember($this->f['tenant']->id, $this->admin->id, $this->adminProfile);
organizationMember($this->f['tenant']->id, $this->second->id, $this->adminProfile);
DB::table('organization_access')->insert(['tenant_id' => $this->f['tenant']->id, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
$this->base = '/administracao/parametrizacao';
$this->actingAs($this->admin);
});
test('setup requires a second named administrator and atomically gives both configured administration', function () {
$new = User::factory()->create();
$this->actingAs($new)->postJson('/empresa/iniciar', ['name' => 'Empresa de entrega QA', 'department' => 'Sede'])->assertUnprocessable()->assertJsonValidationErrors(['second_name', 'second_email', 'second_password']);
$this->post('/empresa/iniciar', ['name' => 'Empresa de entrega QA', 'department' => 'Sede', 'first_limit' => '1000.00', 'second_limit' => '2000.00', 'includes_tax' => true, 'second_name' => 'Segundo administrador QA', 'second_email' => 'segundo-entrega@example.test', 'second_password' => 'QA-Password-123456', 'second_password_confirmation' => 'QA-Password-123456'])->assertRedirect();
$new->refresh();
$second = User::query()->where('email', 'segundo-entrega@example.test')->sole();
expect($second->tenant_id)->toBe($new->tenant_id)->and(Hash::check('QA-Password-123456', $second->password))->toBeTrue();
expect(app(OrganizationAccess::class)->permissions($new))->toBe(['admin'])->and(app(OrganizationAccess::class)->permissions($second))->toBe(['admin']);
$this->actingAs($second)->get($this->base)->assertOk();
});
test('both administrators can access parameters independently of the stored development role', function () {
$this->get($this->base)->assertInertia(fn (Assert $p) => $p->where('active', true)->where('version', 1)->where('profiles.0.name', 'Direcção de sistemas')->has('users')->missing('users.0.password'));
$this->actingAs($this->second)->get($this->base)->assertOk();
$this->get('/empresa')->assertRedirect($this->base);
expect($this->second->purchasing_role)->toBe(PurchasingRole::Requester);
});
test('custom profile grants a real operational permission and the legacy profile grants nothing after activation', function () {
$this->post($this->base.'/profile', ['version' => 1, 'name' => 'Conferência da empresa', 'permissions' => ['finance']])->assertRedirect();
$profile = (int) DB::table('organization_profiles')->where('name', 'Conferência da empresa')->value('id');
$data = ['version' => 2, 'name' => 'Utilizador configurado', 'email' => 'configurado@example.test', 'password' => 'QA-Password-123456', 'password_confirmation' => 'QA-Password-123456', 'department_id' => $this->f['r']->department_id, 'profile_ids' => [$profile], 'reason' => 'Atribuição aprovada pela empresa no ensaio.'];
$this->post($this->base.'/user', $data)->assertRedirect();
$user = User::query()->where('email', $data['email'])->sole();
expect($user->purchasing_role)->toBe(PurchasingRole::Configured)->and($user->hasFunctions(['finance']))->toBeTrue()->and($user->hasFunctions(['admin']))->toBeFalse();
$this->actingAs($user)->get('/facturas')->assertOk();
$this->get('/requisicoes/criar')->assertForbidden();
$this->get($this->base)->assertForbidden();
$legacy = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'purchasing_role' => PurchasingRole::Admin]);
$this->actingAs($legacy)->get('/facturas')->assertForbidden();
$this->get($this->base)->assertForbidden();
});
test('two permanent administrators cannot be removed by assignment profile editing or expiry', function () {
$this->postJson($this->base.'/assign', ['version' => 1, 'user_id' => $this->second->id, 'profile_ids' => [], 'maintenance' => false, 'reason' => 'Tentativa de retirar o segundo administrador.'])->assertUnprocessable()->assertJsonValidationErrors('administrators');
$this->postJson($this->base.'/profile', ['version' => 1, 'id' => $this->adminProfile, 'name' => 'Sem poderes', 'permissions' => []])->assertUnprocessable()->assertJsonValidationErrors('administrators');
$this->postJson($this->base.'/assign', ['version' => 1, 'user_id' => $this->second->id, 'profile_ids' => [$this->adminProfile], 'maintenance' => false, 'expires_at' => now()->addDay()->toIso8601String(), 'reason' => 'Tentativa de tornar temporário o segundo administrador.'])->assertUnprocessable();
expect(DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->value('version'))->toBe(1)->and(DB::table('organization_memberships')->count())->toBe(2);
});
test('maintenance is named temporary read-only audited and expires without bypassing authentication', function () {
$profile = organizationProfile($this->f['tenant']->id, 'Consulta TEMPO', ['finance']);
$support = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['r']->department_id, 'purchasing_role' => PurchasingRole::Admin]);
$grant = ['version' => 1, 'user_id' => $support->id, 'profile_ids' => [$profile], 'maintenance' => true, 'expires_at' => now()->addHour()->toIso8601String(), 'reason' => 'Diagnóstico TEMPO identificado e autorizado no ensaio.'];
$this->post($this->base.'/assign', $grant)->assertRedirect();
$this->actingAs($support)->get('/facturas')->assertOk();
expect(AuditLog::query()->where('action', 'maintenance.consulted')->count())->toBe(1);
$this->post('/requisicoes', [])->assertForbidden();
$this->get($this->base)->assertForbidden();
$this->travel(2)->hours();
$this->get('/facturas')->assertForbidden();
auth()->logout();
$this->get('/facturas')->assertRedirect('/login');
});
test('maintenance cannot receive administration and access can be revoked immediately', function () {
$this->postJson($this->base.'/assign', ['version' => 1, 'user_id' => $this->second->id, 'profile_ids' => [$this->adminProfile], 'maintenance' => true, 'expires_at' => now()->addHour()->toIso8601String(), 'reason' => 'Tentativa de conceder administração à manutenção.'])->assertUnprocessable();
$profile = organizationProfile($this->f['tenant']->id, 'Compras da empresa', ['procurement']);
organizationMember($this->f['tenant']->id, $this->f['receiver']->id, $profile);
$this->actingAs($this->f['receiver'])->get('/fornecedores')->assertOk();
$this->actingAs($this->admin)->post($this->base.'/assign', ['version' => 1, 'user_id' => $this->f['receiver']->id, 'profile_ids' => [], 'maintenance' => false, 'reason' => 'Revogação imediata da atribuição de teste.'])->assertRedirect();
$this->actingAs($this->f['receiver'])->get('/fornecedores')->assertForbidden();
});
test('foreign profiles users forged permissions and stale configuration cannot be used', function () {
$other = receiptFixture();
$foreign = organizationProfile($other['tenant']->id, 'Perfil alheio', ['admin']);
$this->actingAs($this->admin)->postJson($this->base.'/assign', ['version' => 1, 'user_id' => $this->second->id, 'profile_ids' => [$foreign], 'maintenance' => false, 'reason' => 'Tentativa sobre um perfil de outra empresa.'])->assertUnprocessable();
$this->post($this->base.'/assign', ['version' => 1, 'user_id' => $other['issuer']->id, 'profile_ids' => [$this->adminProfile], 'maintenance' => false, 'reason' => 'Tentativa sobre utilizador de outra empresa.'])->assertNotFound();
$this->postJson($this->base.'/profile', ['version' => 1, 'name' => 'Forjado', 'permissions' => ['superuser']])->assertUnprocessable();
$this->postJson($this->base.'/profile', ['version' => 1, 'name' => 'Forjado', 'permissions' => ['finance'], 'tenant_id' => $other['tenant']->id])->assertUnprocessable();
$this->post($this->base.'/profile', ['version' => 1, 'name' => 'Perfil novo', 'permissions' => ['finance']])->assertRedirect();
$this->postJson($this->base.'/profile', ['version' => 1, 'name' => 'Sobreposição', 'permissions' => ['finance']])->assertUnprocessable()->assertJsonValidationErrors('version');
});
test('activation requires two assigned administrators and replaces the previous role authority', function () {
DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->update(['active' => false]);
DB::table('tenants')->where('id', $this->f['tenant']->id)->update(['rules_provisional' => false]);
DB::table('users')->where('id', $this->admin->id)->update(['purchasing_role' => 'admin']);
DB::table('organization_memberships')->where('user_id', $this->second->id)->delete();
$this->actingAs($this->admin->refresh())->postJson($this->base.'/activate', ['version' => 1, 'confirm' => true, 'reason' => 'Activação da configuração escolhida pela empresa.'])->assertUnprocessable();
organizationMember($this->f['tenant']->id, $this->second->id, $this->adminProfile);
$this->post($this->base.'/activate', ['version' => 1, 'confirm' => true, 'reason' => 'Activação da configuração escolhida pela empresa.'])->assertRedirect();
expect(app(OrganizationAccess::class)->active($this->f['tenant']->id))->toBeTrue();
$this->actingAs($this->f['receiver'])->get('/recepcoes')->assertForbidden();
});
test('configuration audit failure rolls a profile and its version back', function () {
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected audit failure'));
$this->withoutExceptionHandling();
expect(fn () => $this->post($this->base.'/profile', ['version' => 1, 'name' => 'Não persistir', 'permissions' => ['finance']]))->toThrow(RuntimeException::class);
expect(DB::table('organization_profiles')->where('name', 'Não persistir')->exists())->toBeFalse()->and(DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->value('version'))->toBe(1);
});
test('company rules replace provisional defaults exactly and preserve existing submitted snapshots', function () {
$before = $this->f['r']->refresh()->approval_snapshot;
$this->post($this->base.'/rules', ['version' => 1, 'first_limit' => '1234.56', 'second_limit' => '9876.54', 'includes_tax' => false, 'reason' => 'Limites expressamente definidos pela empresa no ensaio.'])->assertRedirect();
$company = $this->f['tenant']->refresh();
expect($company->first_limit_minor)->toBe(123456)->and($company->second_limit_minor)->toBe(987654)->and($company->rules_provisional)->toBeFalse()->and($company->approval_includes_tax)->toBeFalse();
expect($this->f['r']->refresh()->approval_snapshot)->toBe($before);
$this->postJson($this->base.'/rules', ['version' => 2, 'first_limit' => '2000.00', 'second_limit' => '1000.00', 'includes_tax' => true, 'reason' => 'Intervalo incoerente deve ser recusado.'])->assertUnprocessable()->assertJsonValidationErrors('second_limit');
});
test('maintenance profile cannot acquire administration by editing its permissions', function () {
$profile = organizationProfile($this->f['tenant']->id, 'Diagnóstico temporário', ['finance']);
$support = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['r']->department_id]);
$this->post($this->base.'/assign', ['version' => 1, 'user_id' => $support->id, 'profile_ids' => [$profile], 'maintenance' => true, 'expires_at' => now()->addHour()->toIso8601String(), 'reason' => 'Acesso temporário nominativo para diagnóstico.'])->assertRedirect();
$this->postJson($this->base.'/profile', ['version' => 2, 'id' => $profile, 'name' => 'Diagnóstico temporário', 'permissions' => ['finance', 'admin']])->assertUnprocessable()->assertJsonValidationErrors('permissions');
expect($support->hasFunctions(['admin']))->toBeFalse();
});
test('the product default seeder does not create demonstration accounts or change existing users', function () {
$before = User::query()->orderBy('id')->get()->map(fn (User $u) => $u->getAttributes())->all();
$this->seed(DatabaseSeeder::class);
expect(User::query()->orderBy('id')->get()->map(fn (User $u) => $u->getAttributes())->all())->toBe($before);
expect(User::query()->where('email', 'test@example.com')->exists())->toBeFalse();
});
test('a configured account has no fallback development permissions while activation is pending', function () {
$staged = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['r']->department_id, 'purchasing_role' => PurchasingRole::Configured]);
organizationMember($this->f['tenant']->id, $staged->id, $this->adminProfile);
DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->update(['active' => false]);
$this->actingAs($staged)->get($this->base)->assertForbidden();
$this->get('/requisicoes/criar')->assertForbidden();
expect(app(OrganizationAccess::class)->canRequest($staged))->toBeFalse();
DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->update(['active' => true]);
$this->get($this->base)->assertOk();
});