commit9
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-30 20:02:30 +02:00
parent 6d14b4e6f0
commit c6373365bf
225 changed files with 15365 additions and 285 deletions
@@ -8,9 +8,11 @@ use App\Services\RequisitionWorkflow;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
require_once __DIR__.'/../Support/AiPlanFixture.php';
function organizationProfile(int $tenant, string $name, array $permissions): int
{
@@ -34,6 +36,101 @@ beforeEach(function () {
$this->actingAs($this->admin);
});
test('intelligence settings are independently configurable audited and scoped without rewriting processes', function () {
$before = $this->f['r']->fresh()->toArray();
$body = ['version' => 1, 'procurement_brief' => false, 'people_brief' => true, 'reason' => 'Apresentar sínteses apenas no percurso de Pessoas.'];
$this->postJson($this->base.'/intelligence', $body)->assertRedirect();
expect($this->f['tenant']->fresh()->intelligence_policy)->toBe(['revision' => 1, 'configured' => true, 'procurement_brief' => false, 'people_brief' => true])
->and($this->f['r']->fresh()->toArray())->toBe($before);
$this->get($this->base)->assertInertia(fn (Assert $page) => $page->where('intelligenceReady', true)->where('intelligencePolicy.procurement_brief', false));
$this->get('/requisicoes/'.$this->f['r']->id.'/relatorio')->assertInertia(fn (Assert $page) => $page->where('brief', null)->where('report.requisition.id', $this->f['r']->id));
$this->postJson($this->base.'/intelligence', $body)->assertUnprocessable();
$this->postJson($this->base.'/intelligence', [...$body, 'version' => 2, 'procurement_brief' => true, 'people_brief' => false])->assertRedirect();
$this->get('/requisicoes/'.$this->f['r']->id.'/relatorio')->assertInertia(fn (Assert $page) => $page->where('brief.method', 'procurement_evidence_brief_v1'));
expect($this->f['tenant']->fresh()->intelligence_policy['revision'])->toBe(2)
->and(AuditLog::query()->where('action', 'organization.intelligence')->count())->toBe(2);
});
test('intelligence configuration rejects unauthorised actors secret fields external activation and cross company targets', function () {
$body = ['version' => 1, 'procurement_brief' => false, 'people_brief' => false, 'reason' => 'Configuração de sínteses da organização.'];
foreach (['api_key' => 'DO-NOT-STORE', 'external_processing_enabled' => true, 'tenant_id' => $this->f['tenant']->id + 1, 'procurement_brief' => 'invalid'] as $key => $value) {
$this->postJson($this->base.'/intelligence', [...$body, $key => $value])->assertUnprocessable();
}
$this->actingAs($this->f['receiver'])->postJson($this->base.'/intelligence', $body)->assertForbidden();
expect($this->f['tenant']->fresh()->intelligence_policy)->toBeNull()
->and(AuditLog::query()->where('action', 'organization.intelligence')->count())->toBe(0);
});
test('intelligence policy and global version roll back when audit cannot be recorded', function () {
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected audit failure'));
$this->withoutExceptionHandling();
expect(fn () => $this->postJson($this->base.'/intelligence', ['version' => 1, 'procurement_brief' => false, 'people_brief' => false, 'reason' => 'Não persistir sem registo de auditoria.']))->toThrow(RuntimeException::class);
expect($this->f['tenant']->fresh()->intelligence_policy)->toBeNull()
->and(DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->value('version'))->toBe(1);
});
test('AI plan is versioned audited and preserved when local summary choices change without activating a provider', function () {
Http::fake();
$before = $this->f['r']->fresh()->toArray();
$this->postJson($this->base.'/intelligence', ['version' => 1, 'procurement_brief' => false, 'people_brief' => true, 'reason' => 'Ocultar a síntese local de Compras.'])->assertRedirect();
$this->postJson($this->base.'/ai_plan', [...aiPlanInput(2), 'monthly_budget_usd' => '10'])->assertRedirect();
$policy = $this->f['tenant']->fresh()->intelligence_policy;
expect($policy['procurement_brief'])->toBeFalse()->and($policy['revision'])->toBe(2)
->and($policy['ai_plan']['state'])->toBe('draft')->and($policy['ai_plan']['monthly_budget_usd'])->toBe('10.00');
$this->get($this->base)->assertInertia(fn (Assert $page) => $page->where('aiPlan.provider', 'openai')->where('aiPlan.state', 'draft'));
$this->postJson($this->base.'/intelligence', ['version' => 3, 'procurement_brief' => true, 'people_brief' => false, 'reason' => 'Mudar apresentação sem apagar o plano.'])->assertRedirect();
expect($this->f['tenant']->fresh()->intelligence_policy['ai_plan'])->toBe($policy['ai_plan'])
->and($this->f['r']->fresh()->toArray())->toBe($before)
->and(AuditLog::query()->where('action', 'organization.ai_plan')->count())->toBe(1);
$this->postJson($this->base.'/ai_plan', aiPlanInput(3))->assertUnprocessable();
Http::assertNotSent(fn ($request) => ! in_array(parse_url($request->url(), PHP_URL_HOST), ['127.0.0.1', 'localhost'], true));
});
test('AI plan rejects secret endpoints activation invalid scopes and unbounded or incomplete settings', function () {
foreach ([['api_key' => 'NOT-A-SECRET'], ['endpoint' => 'https://invalid.example'], ['state' => 'active'], ['external_processing_enabled' => true],
['provider' => 'unknown'], ['model' => 'https://invalid.example/model'], ['model' => 'sk-fictitious-not-a-real-key'], ['domains' => ['unknown']], ['data_scopes' => ['raw_candidate_cv']],
['domains' => []], ['monthly_budget_usd' => '0'], ['monthly_requests' => 0], ['max_input_bytes' => 64001], ['max_output_tokens' => 4001]] as $bad) {
$this->postJson($this->base.'/ai_plan', [...aiPlanInput(), ...$bad])->assertUnprocessable();
}
expect($this->f['tenant']->fresh()->intelligence_policy)->toBeNull()
->and(AuditLog::query()->where('action', 'organization.ai_plan')->count())->toBe(0);
$this->actingAs($this->f['receiver'])->postJson($this->base.'/ai_plan', aiPlanInput())->assertForbidden();
});
test('AI plan rolls back with the shared configuration version when audit fails', function () {
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected audit failure'));
$this->withoutExceptionHandling();
expect(fn () => $this->postJson($this->base.'/ai_plan', aiPlanInput()))->toThrow(RuntimeException::class);
expect($this->f['tenant']->fresh()->intelligence_policy)->toBeNull()
->and(DB::table('organization_access')->where('tenant_id', $this->f['tenant']->id)->value('version'))->toBe(1);
});
test('tax configuration is versioned tenant scoped and never rewrites existing process lines', function () {
$before = $this->f['r']->items()->get()->toArray();
$this->postJson($this->base.'/tax', ['version' => 1, 'default_tax_rate' => '5.00', 'reason' => 'Enquadramento fiscal específico da empresa.'])->assertRedirect();
expect($this->f['tenant']->fresh()->default_tax_rate)->toBe('5.00')
->and($this->f['r']->items()->get()->toArray())->toBe($before);
$this->get('/requisicoes/criar')->assertForbidden();
$this->postJson($this->base.'/tax', ['version' => 1, 'default_tax_rate' => '16', 'reason' => 'Pedido com versão antiga não pode gravar.'])->assertUnprocessable();
$this->postJson($this->base.'/tax', ['version' => 2, 'default_tax_rate' => '101', 'reason' => 'Taxa inválida não pode ser gravada.'])->assertUnprocessable();
expect(AuditLog::query()->where('action', 'organization.tax')->count())->toBe(1);
$this->actingAs($this->f['receiver'])->postJson($this->base.'/tax', ['version' => 2, 'default_tax_rate' => '16', 'reason' => 'Sem competência de administração.'])->assertForbidden();
});
test('comparison policy requires valid weights authority and current version without rewriting existing processes', function () {
$body = ['version' => 1, 'minimum_proposals' => 2, 'minimum_evaluators' => 1, 'weights' => ['price' => 50, 'quality' => 20, 'delivery' => 10, 'history' => 10, 'payment' => 10], 'reason' => 'Regra comercial aprovada para novos processos.'];
$this->postJson($this->base.'/comparison', [...$body, 'weights' => [...$body['weights'], 'price' => 49]])->assertUnprocessable();
expect($this->f['tenant']->fresh()->comparison_policy)->toBeNull();
$this->postJson($this->base.'/comparison', [...$body, 'minimum_evaluators' => 0])->assertUnprocessable();
$this->postJson($this->base.'/comparison', [...$body, 'weights' => [...$body['weights'], 'other' => 0]])->assertUnprocessable();
$this->postJson($this->base.'/comparison', $body)->assertRedirect();
expect($this->f['tenant']->fresh()->comparison_policy['revision'])->toBe(1)->and($this->f['r']->fresh()->comparison_policy)->toBeNull();
$this->postJson($this->base.'/comparison', $body)->assertUnprocessable();
$this->get($this->base)->assertInertia(fn (Assert $page) => $page->where('comparisonPolicy.minimum_proposals', 2));
expect(AuditLog::query()->where('action', 'organization.comparison')->count())->toBe(1);
$this->actingAs($this->f['receiver'])->postJson($this->base.'/comparison', [...$body, 'version' => 2])->assertForbidden();
});
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']);