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(); });