148 lines
12 KiB
PHP
148 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Enums\PurchasingRole;
|
|
use App\Models\AuditLog;
|
|
use App\Models\Department;
|
|
use App\Models\Supplier;
|
|
use App\Models\SupplierDocument;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\RequisitionWorkflow;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
|
|
function maintenanceSupplierData(): array
|
|
{
|
|
return ['name' => 'Fornecedor existente', 'nuit' => '123456789', 'contact' => 'Contacto comercial', 'email' => 'fornecedor@example.test', 'phone' => '123456789', 'address' => 'Maputo', 'category' => 'Material', 'beneficiaries' => 'Beneficiário de teste', 'document_references' => 'Documentos verificados no processo de teste.', 'diligence_notes' => 'Documentação conferida para testes de manutenção.', 'blocked' => false, 'next_review' => now()->addYear()->format('Y-m-d')];
|
|
}
|
|
|
|
beforeEach(function () {
|
|
Storage::fake('supplier_documents');
|
|
$this->tenant = Tenant::query()->create(['name' => 'Manutenção A']);
|
|
$department = Department::query()->create(['tenant_id' => $this->tenant->id, 'name' => 'Operações']);
|
|
$this->buyer = User::factory()->create(['tenant_id' => $this->tenant->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::Procurement]);
|
|
$this->finance = User::factory()->create(['tenant_id' => $this->tenant->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::Finance]);
|
|
$this->actingAs($this->buyer);
|
|
$this->supplier = Supplier::query()->create([...maintenanceSupplierData(), 'tenant_id' => $this->tenant->id, 'created_by' => $this->buyer->id, 'status' => 'active', 'reviewed_by' => $this->finance->id, 'review_comment' => 'Validação anterior preservada em auditoria.']);
|
|
$this->url = '/fornecedores/'.$this->supplier->id;
|
|
});
|
|
|
|
test('procurement can edit and renew a supplier but cannot forge validation or ownership', function () {
|
|
$this->get($this->url.'/editar')->assertInertia(fn (Assert $page) => $page->component('suppliers/edit')->where('supplier.version', 1));
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1, 'name' => 'Nome actualizado', 'tenant_id' => 999, 'created_by' => 999, 'status' => 'active', 'reviewed_by' => 999])->assertSessionHasNoErrors()->assertRedirect($this->url);
|
|
$supplier = $this->supplier->refresh();
|
|
expect($supplier->name)->toBe('Nome actualizado')->and($supplier->status)->toBe('pending')->and($supplier->version)->toBe(2);
|
|
expect($supplier->tenant_id)->toBe($this->tenant->id)->and($supplier->created_by)->toBe($this->buyer->id)->and($supplier->reviewed_by)->toBeNull()->and($supplier->last_changed_by)->toBe($this->buyer->id);
|
|
expect($supplier->eligible())->toBeFalse();
|
|
$audit = AuditLog::query()->where('action', 'supplier.updated')->firstOrFail();
|
|
expect($audit->before['name'])->toBe('Fornecedor existente')->and($audit->after['name'])->toBe('Nome actualizado');
|
|
});
|
|
|
|
test('unchanged edits preserve validation and do not create audit noise', function () {
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1])->assertSessionHasNoErrors();
|
|
expect($this->supplier->refresh()->status)->toBe('active')->and($this->supplier->version)->toBe(1);
|
|
expect(AuditLog::query()->where('action', 'supplier.updated')->count())->toBe(0);
|
|
});
|
|
|
|
test('stale edits cannot overwrite newer supplier data', function () {
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1, 'name' => 'Actualização concorrente'])->assertSessionHasNoErrors();
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1, 'name' => 'Ficha antiga'])->assertSessionHasErrors('version');
|
|
expect($this->supplier->refresh()->name)->toBe('Actualização concorrente');
|
|
});
|
|
|
|
test('editing validates nuit uniqueness within company and renewal date', function () {
|
|
Supplier::query()->create([...maintenanceSupplierData(), 'nuit' => '987654321', 'tenant_id' => $this->tenant->id, 'created_by' => $this->buyer->id]);
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1, 'nuit' => '987654321'])->assertSessionHasErrors('nuit');
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1, 'next_review' => now()->subDay()->format('Y-m-d')])->assertSessionHasErrors('next_review');
|
|
});
|
|
|
|
test('non procurement roles cannot change suppliers or upload documents', function (PurchasingRole $role) {
|
|
$user = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => $role]);
|
|
$this->actingAs($user)->get($this->url.'/editar')->assertForbidden();
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 1])->assertForbidden();
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 10, 'application/pdf')])->assertForbidden();
|
|
expect(SupplierDocument::query()->count())->toBe(0);
|
|
})->with([PurchasingRole::Finance, PurchasingRole::Admin, PurchasingRole::Requester, PurchasingRole::DepartmentHead, PurchasingRole::Warehouse]);
|
|
|
|
test('private upload saves metadata invalidates validation and audits without leaking storage paths', function () {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 10, 'application/pdf')])->assertSessionHasNoErrors()->assertRedirect();
|
|
$document = SupplierDocument::query()->firstOrFail();
|
|
Storage::disk('supplier_documents')->assertExists($document->path);
|
|
expect($document->path)->toStartWith($this->tenant->id.'/'.$this->supplier->id.'/');
|
|
expect($document->original_name)->toBe('alvara.pdf')->and(strlen($document->sha256))->toBe(64);
|
|
expect($document->toArray())->not->toHaveKey('path');
|
|
expect($this->supplier->refresh()->status)->toBe('pending')->and($this->supplier->version)->toBe(2);
|
|
expect(AuditLog::query()->where('action', 'supplier.document_uploaded')->firstOrFail()->after['document'])->not->toHaveKey('path');
|
|
$this->get($this->url)->assertInertia(fn (Assert $page) => $page->component('suppliers/show')->has('documents', 1)->missing('documents.0.path')->where('canEdit', true)->where('canDocuments', true));
|
|
$this->get('/storage/'.$document->path)->assertForbidden();
|
|
});
|
|
|
|
test('authorised downloads are attachments and recorded in audit', function (PurchasingRole $role) {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'tax', 'file' => UploadedFile::fake()->create('certidao.pdf', 10, 'application/pdf')]);
|
|
$document = SupplierDocument::query()->firstOrFail();
|
|
$user = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => $role]);
|
|
$this->actingAs($user)->get($this->url.'/documentos/'.$document->id)->assertOk()->assertDownload('certidao.pdf')->assertHeader('X-Content-Type-Options', 'nosniff')->assertHeader('Content-Type', 'application/octet-stream');
|
|
expect(AuditLog::query()->where('action', 'supplier.document_downloaded')->count())->toBe(1);
|
|
})->with([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance]);
|
|
|
|
test('heads see the supplier but cannot inspect or download confidential documents', function () {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'conflict', 'file' => UploadedFile::fake()->create('conflitos.pdf', 10, 'application/pdf')]);
|
|
$document = SupplierDocument::query()->firstOrFail();
|
|
$head = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::DepartmentHead]);
|
|
$this->actingAs($head)->get($this->url)->assertInertia(fn (Assert $page) => $page->has('documents', 0)->where('canDocuments', false));
|
|
$this->get($this->url.'/documentos/'.$document->id)->assertForbidden();
|
|
});
|
|
|
|
test('uploads validate contents extension size and document classification', function () {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('script.php', 1, 'text/x-php')])->assertSessionHasErrors('file');
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('script.php', 1, 'application/pdf')])->assertSessionHasErrors('file');
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('grande.pdf', 2049, 'application/pdf')])->assertSessionHasErrors('file');
|
|
$this->post($this->url.'/documentos', ['document_type' => 'invalid', 'file' => UploadedFile::fake()->create('alvara.pdf', 1, 'application/pdf')])->assertSessionHasErrors('document_type');
|
|
expect(SupplierDocument::query()->count())->toBe(0)->and(Storage::disk('supplier_documents')->allFiles())->toBe([]);
|
|
expect($this->supplier->refresh()->status)->toBe('active');
|
|
});
|
|
|
|
test('same company document cannot be downloaded under another supplier', function () {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 1, 'application/pdf')]);
|
|
$document = SupplierDocument::query()->firstOrFail();
|
|
$other = Supplier::query()->create([...maintenanceSupplierData(), 'nuit' => '987654321', 'tenant_id' => $this->tenant->id, 'created_by' => $this->buyer->id]);
|
|
$this->get('/fornecedores/'.$other->id.'/documentos/'.$document->id)->assertNotFound();
|
|
expect(AuditLog::query()->where('action', 'supplier.document_downloaded')->count())->toBe(0);
|
|
});
|
|
|
|
test('all maintenance and attachment routes reject other companies', function () {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 1, 'application/pdf')]);
|
|
$document = SupplierDocument::query()->firstOrFail();
|
|
$other = Tenant::query()->create(['name' => 'Empresa B']);
|
|
$user = User::factory()->create(['tenant_id' => $other->id, 'purchasing_role' => PurchasingRole::Procurement]);
|
|
$this->actingAs($user)->get($this->url)->assertNotFound();
|
|
$this->get($this->url.'/editar')->assertNotFound();
|
|
$this->put($this->url, [...maintenanceSupplierData(), 'version' => 2])->assertNotFound();
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 1, 'application/pdf')])->assertNotFound();
|
|
$this->get($this->url.'/documentos/'.$document->id)->assertNotFound();
|
|
});
|
|
|
|
test('review of a stale version is rejected after a new document upload', function () {
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 1, 'application/pdf')]);
|
|
$this->actingAs($this->finance)->post($this->url.'/validar', ['status' => 'active', 'comment' => 'Revisão de ficha antiga não permitida.', 'version' => 1])->assertSessionHasErrors('version');
|
|
$this->post($this->url.'/validar', ['status' => 'active', 'comment' => 'Dados e documento actual conferidos.', 'version' => 2])->assertSessionHasNoErrors();
|
|
expect($this->supplier->refresh()->status)->toBe('active')->and($this->supplier->version)->toBe(3);
|
|
});
|
|
|
|
test('last editor cannot validate the supplier after changing to finance role', function () {
|
|
$editor = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Procurement]);
|
|
$this->actingAs($editor)->put($this->url, [...maintenanceSupplierData(), 'version' => 1, 'contact' => 'Novo contacto'])->assertSessionHasNoErrors();
|
|
$editor->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
|
|
$this->actingAs($editor)->post($this->url.'/validar', ['status' => 'active', 'comment' => 'Tentativa de validar alteração própria.', 'version' => 2])->assertForbidden();
|
|
});
|
|
|
|
test('file is cleaned up and database rolled back if audit cannot be written', function () {
|
|
$this->mock(RequisitionWorkflow::class, function ($mock) {
|
|
$mock->shouldReceive('audit')->once()->andThrow(new RuntimeException('Falha de auditoria simulada.'));
|
|
});
|
|
$this->post($this->url.'/documentos', ['document_type' => 'license', 'file' => UploadedFile::fake()->create('alvara.pdf', 1, 'application/pdf')])->assertStatus(500);
|
|
expect(SupplierDocument::query()->count())->toBe(0)->and(Storage::disk('supplier_documents')->allFiles())->toBe([]);
|
|
expect($this->supplier->refresh()->status)->toBe('active')->and($this->supplier->version)->toBe(1);
|
|
});
|