Files
compras/tests/Feature/WorkContextTest.php
T
2026-11-02 08:08:05 +02:00

149 lines
10 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\AuditLog;
use App\Models\PeopleProcess;
use App\Models\Requisition;
use App\Models\User;
use App\Services\OrganizationAccess;
use App\Services\PeopleContractHandoff;
use App\Services\WorkContext;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia as Assert;
use Symfony\Component\HttpKernel\Exception\HttpException;
require_once __DIR__.'/../Support/ReceiptFixture.php';
beforeEach(function () {
$this->f = receiptFixture();
$this->actor = $this->f['issuer'];
$this->actor->forceFill(['purchasing_role' => PurchasingRole::Admin])->save();
$this->f['tenant']->update(['rules_provisional' => false, 'work_context_policy' => ['revision' => 1, 'enabled' => array_keys(WorkContext::OPTIONS)]]);
config(['work_context.entitlements.'.$this->actor->tenant_id => array_keys(WorkContext::OPTIONS)]);
$this->actingAs($this->actor);
});
test('selection intersects product configuration and permission without granting functions', function () {
$before = app(OrganizationAccess::class)->permissions($this->actor);
config(['work_context.entitlements.'.$this->actor->tenant_id => ['procurement.internal']]);
$this->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertForbidden();
$this->post('/contexto', ['area' => 'procurement'])->assertRedirect('/dashboard');
expect(session('work_context.key'))->toBe('procurement.internal')
->and(app(OrganizationAccess::class)->permissions($this->actor))->toBe($before);
$this->get('/requisicoes')->assertOk();
$this->get('/pessoas')->assertForbidden();
$this->get('/pessoas/relatorios')->assertForbidden();
$this->get('/pessoas/relatorios.csv')->assertForbidden();
$this->post('/pessoas', [])->assertForbidden();
});
test('two choices require explicit option and none exposes no business data on dashboard', function () {
$this->post('/contexto', ['area' => 'procurement'])->assertForbidden();
$this->get('/dashboard')->assertOk()->assertInertia(fn (Assert $page) => $page->component('work-context')->missing('stats')->missing('recent')->where('context.active', null));
config(['work_context.entitlements.'.$this->actor->tenant_id => []]);
$this->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('context.available', []));
$this->get('/requisicoes')->assertForbidden();
});
test('context changes invalidate old area operations and do not change submitted records', function () {
$before = $this->f['r']->fresh()->toArray();
$this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect();
$this->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertRedirect();
$this->get('/requisicoes/'.$this->f['r']->id)->assertNotFound();
$this->get('/requisicoes/'.$this->f['r']->id.'/relatorio')->assertNotFound();
$this->post('/requisicoes/'.$this->f['r']->id.'/submeter')->assertNotFound();
expect(Requisition::withoutGlobalScope('work_context')->findOrFail($this->f['r']->id)->toArray())->toBe($before);
});
test('commercial withdrawal invalidates active context and pending options cannot reuse internal workflows', function () {
$this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect();
config(['work_context.entitlements.'.$this->actor->tenant_id => ['procurement.sales']]);
$this->get('/requisicoes')->assertForbidden();
$this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.sales'])->assertRedirect();
$this->get('/requisicoes')->assertRedirect('/dashboard');
$this->post('/requisicoes', [])->assertStatus(409);
});
test('people reports in a pending modality return to the context panel without exposing internal data', function () {
$this->post('/contexto', ['area' => 'people', 'option' => 'people.third_party'])->assertRedirect('/dashboard');
$this->get('/pessoas/relatorios')->assertRedirect('/dashboard');
$this->get('/pessoas/relatorios.csv')->assertRedirect('/dashboard');
$this->get('/dashboard')->assertOk()->assertInertia(fn (Assert $page) => $page
->component('work-context')->where('context.active', 'people.third_party')->missing('processes'));
$this->post('/pessoas', [])->assertStatus(409);
$this->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertRedirect('/dashboard');
$this->get('/pessoas/relatorios')->assertOk()->assertInertia(fn (Assert $page) => $page->component('people/reports'));
});
test('context is bound to account and company and rejects area mismatches', function () {
$this->post('/contexto', ['area' => 'procurement', 'option' => 'people.internal'])->assertForbidden();
$this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect();
$other = User::factory()->create(['tenant_id' => $this->actor->tenant_id, 'department_id' => $this->actor->department_id, 'purchasing_role' => PurchasingRole::Admin]);
$this->actingAs($other)->get('/requisicoes')->assertForbidden();
expect(AuditLog::query()->where('action', 'work_context.selected')->count())->toBe(1);
});
test('area profiles do not combine permissions from other contexts and expired membership grants nothing', function () {
$tenant = $this->actor->tenant_id;
DB::table('organization_access')->insert(['tenant_id' => $tenant, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
foreach ([['procurement.internal', ['requester']], ['people.internal', ['admin']]] as [$key, $permissions]) {
$profile = DB::table('organization_profiles')->insertGetId(['tenant_id' => $tenant, 'name' => $key, 'permissions' => json_encode($permissions), 'work_contexts' => json_encode([$key]), 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
DB::table('organization_memberships')->insert(['tenant_id' => $tenant, 'user_id' => $this->actor->id, 'profile_id' => $profile, 'maintenance' => false, 'created_at' => now(), 'updated_at' => now()]);
}
$this->post('/contexto', ['area' => 'procurement'])->assertRedirect();
$this->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('context.permissions', ['requester']));
$this->get('/requisicoes')->assertInertia(fn (Assert $page) => $page->where('organizationAccess.permissions', ['requester']));
DB::table('organization_memberships')->where('tenant_id', $tenant)->update(['expires_at' => now()->subMinute()]);
$this->get('/requisicoes')->assertForbidden();
});
test('product configuration cannot enable unpurchased options and changes are versioned audited', function () {
config(['work_context.entitlements.'.$this->actor->tenant_id => ['procurement.internal']]);
$body = ['version' => 1, 'enabled' => ['people.internal'], 'confirm' => true, 'reason' => 'Definir as opções adquiridas pela empresa.'];
$this->postJson('/administracao/parametrizacao/work_contexts', $body)->assertUnprocessable();
$body['enabled'] = ['procurement.internal'];
$this->postJson('/administracao/parametrizacao/work_contexts', $body)->assertRedirect();
$this->postJson('/administracao/parametrizacao/work_contexts', $body)->assertUnprocessable();
expect($this->f['tenant']->fresh()->work_context_policy)->toBe(['revision' => 2, 'enabled' => ['procurement.internal']])
->and(AuditLog::query()->where('action', 'organization.work_contexts')->count())->toBe(1);
});
test('SGF composition excludes third party hiring even if installation requests it', function () {
config(['work_context.composition' => 'sgf']);
expect(app(WorkContext::class)->purchased($this->actor))->not->toContain('people.third_party');
$this->post('/contexto', ['area' => 'people', 'option' => 'people.third_party'])->assertForbidden();
});
test('origin classification is internal and SQL cannot transfer a process to another context', function () {
expect($this->f['r']->fresh()->work_context_key)->toBe('procurement.internal');
expect(fn () => DB::table('requisitions')->where('id', $this->f['r']->id)->update(['work_context_key' => 'procurement.sales']))->toThrow(QueryException::class);
});
test('RH boundary refuses selection and draft contracts and performs no external request', function () {
Http::fake();
foreach (['selected', 'selection_review', 'contract_draft'] as $status) {
$record = new PeopleProcess(['tenant_id' => $this->actor->tenant_id, 'status' => $status, 'work_context_key' => 'people.internal', 'contract' => ['status' => 'draft']]);
expect(fn () => app(PeopleContractHandoff::class)->envelope($record))->toThrow(HttpException::class);
}
Http::assertNothingSent();
});
test('delivery check refuses the development fixture without deleting its records', function () {
$before = DB::table('requisitions')->count();
$this->artisan('sgc:delivery-check', ['tenant' => $this->actor->tenant_id])->assertFailed();
expect(DB::table('requisitions')->count())->toBe($before);
});
test('temporary maintenance may select its authorised context but cannot execute business commands', function () {
$tenant = $this->actor->tenant_id;
DB::table('organization_access')->insert(['tenant_id' => $tenant, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
$profile = DB::table('organization_profiles')->insertGetId(['tenant_id' => $tenant, 'name' => 'Consulta temporária', 'permissions' => json_encode(['procurement']), 'work_contexts' => json_encode(['procurement.internal']), 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
DB::table('organization_memberships')->insert(['tenant_id' => $tenant, 'user_id' => $this->actor->id, 'profile_id' => $profile, 'maintenance' => true, 'expires_at' => now()->addHour(), 'created_at' => now(), 'updated_at' => now()]);
$this->post('/contexto', ['area' => 'procurement'])->assertRedirect('/dashboard');
$this->get('/requisicoes')->assertOk();
$this->post('/requisicoes', [])->assertForbidden();
$this->post('/administracao/parametrizacao/work_contexts', [])->assertForbidden();
});