128 lines
8.1 KiB
PHP
128 lines
8.1 KiB
PHP
<?php
|
|
|
|
use App\Models\GoodsReceipt;
|
|
use App\Models\ReceiptDocument;
|
|
use App\Services\ReceiptDocuments;
|
|
use App\Services\ReceiptWorkflow;
|
|
use App\Services\RequisitionWorkflow;
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
|
|
require_once __DIR__.'/../Support/ReceiptFixture.php';
|
|
require_once __DIR__.'/../Support/SupplyFixture.php';
|
|
|
|
beforeEach(function () {
|
|
$this->f = receiptFixture();
|
|
$this->actingAs($this->f['receiver']);
|
|
$this->receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f));
|
|
$this->directory = storage_path('framework/testing/receipt-block-'.Str::uuid());
|
|
Storage::set('receipt_documents', Storage::build(['driver' => 'local', 'root' => $this->directory, 'throw' => true]));
|
|
$this->image = UploadedFile::fake()->createWithContent('guia.png', base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6E4sAAAAASUVORK5CYII='));
|
|
$this->key = (string) Str::uuid();
|
|
$this->url = '/recepcoes/'.$this->receipt->id;
|
|
});
|
|
|
|
afterEach(function () {
|
|
File::deleteDirectory($this->directory);
|
|
});
|
|
|
|
function receiptDocumentInput(object $test, ?string $key = null, ?UploadedFile $file = null): array
|
|
{
|
|
return ['operation_key' => $key ?? $test->key, 'document_type' => 'delivery_note', 'file' => $file ?? $test->image];
|
|
}
|
|
|
|
test('receipt document block uploads reuses and downloads evidence without repeating receipt stock or approvals', function () {
|
|
$receiptBefore = $this->receipt->getAttributes();
|
|
$before = [];
|
|
foreach (['goods_receipts', 'goods_receipt_items', 'stock_movements', 'approval_steps', 'budget_entries', 'purchase_orders'] as $table) {
|
|
$before[$table] = DB::table($table)->orderBy('id')->get()->toArray();
|
|
}
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect($this->url.'#receipt-documents');
|
|
$document = ReceiptDocument::query()->sole();
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect();
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertRedirect();
|
|
expect(ReceiptDocument::query()->count())->toBe(1)->and(Storage::disk('receipt_documents')->allFiles())->toHaveCount(1);
|
|
expect(DB::table('audit_logs')->where('action', 'receipt.document_added')->count())->toBe(1);
|
|
expect(DB::table('audit_logs')->where('action', 'receipt.document_reused')->count())->toBe(1);
|
|
$this->get($this->url)->assertInertia(fn (Assert $p) => $p->where('documentsReady', true)->where('canAddDocuments', true)
|
|
->has('documents', 1)->missing('documents.0.path')->where('documents.0.id', $document->id));
|
|
$response = $this->get($this->url.'/documentos/'.$document->id)->assertOk()->assertHeader('Content-Type', 'application/octet-stream')
|
|
->assertHeader('X-Content-Type-Options', 'nosniff')->assertHeader('Cache-Control', 'no-store, private');
|
|
expect(hash('sha256', $response->streamedContent()))->toBe($document->sha256);
|
|
foreach ($before as $table => $rows) {
|
|
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows);
|
|
}
|
|
expect($this->receipt->fresh()->getAttributes())->toBe($receiptBefore);
|
|
});
|
|
|
|
test('receipt document block preserves role independence tenant and receipt scopes', function () {
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect();
|
|
$document = ReceiptDocument::query()->sole();
|
|
$this->actingAs($this->f['issuer'])->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertForbidden();
|
|
$this->get($this->url.'/documentos/'.$document->id)->assertOk();
|
|
supplyGrant($this->f['receiver'], ['warehouse']);
|
|
supplyGrant($this->f['issuer'], ['warehouse']);
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertForbidden();
|
|
$foreign = receiptFixture();
|
|
$this->actingAs($foreign['receiver'])->get($this->url.'/documentos/'.$document->id)->assertNotFound();
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this))->assertNotFound();
|
|
$this->actingAs($this->f['receiver']);
|
|
$another = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '0.100'));
|
|
$this->get('/recepcoes/'.$another->id.'/documentos/'.$document->id)->assertNotFound();
|
|
supplyGrant($this->f['receiver'], ['warehouse']);
|
|
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true]);
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertForbidden();
|
|
$this->get($this->url.'/documentos/'.$document->id)->assertForbidden();
|
|
});
|
|
|
|
test('receipt document block rejects changed replay and invalid files and detects tampering', function () {
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect();
|
|
$document = ReceiptDocument::query()->sole();
|
|
$this->withHeader('Accept', 'application/json')->post($this->url.'/documentos', receiptDocumentInput($this, null, UploadedFile::fake()->createWithContent('guia.png', file_get_contents($this->image->getPathname()).'changed')))->assertUnprocessable();
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid(), UploadedFile::fake()->create('bad.html', 1, 'text/html')))->assertUnprocessable();
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid(), UploadedFile::fake()->create('large.pdf', 2049, 'application/pdf')))->assertUnprocessable();
|
|
expect(ReceiptDocument::query()->count())->toBe(1)->and(Storage::disk('receipt_documents')->allFiles())->toHaveCount(1);
|
|
foreach (['update', 'delete'] as $operation) {
|
|
try {
|
|
DB::transaction(function () use ($document, $operation) {
|
|
$query = DB::table('receipt_documents')->where('id', $document->id);
|
|
$operation === 'update' ? $query->update(['original_name' => 'changed.png']) : $query->delete();
|
|
});
|
|
test()->fail('Document metadata must remain append-only');
|
|
} catch (QueryException) {
|
|
expect($document->fresh()->original_name)->toBe('guia.png');
|
|
}
|
|
}
|
|
Storage::disk('receipt_documents')->put($document->path, 'TAMPERED');
|
|
$this->get($this->url.'/documentos/'.$document->id)->assertStatus(409);
|
|
Storage::disk('receipt_documents')->delete($document->path);
|
|
$this->get($this->url.'/documentos/'.$document->id)->assertNotFound();
|
|
});
|
|
|
|
test('receipt document block rolls back metadata and private file if audit fails then permits safe retry', function () {
|
|
$this->partialMock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected document audit failure'));
|
|
try {
|
|
app(ReceiptDocuments::class)->add($this->f['receiver'], $this->receipt, receiptDocumentInput($this), $this->image);
|
|
test()->fail('Expected rollback');
|
|
} catch (RuntimeException $error) {
|
|
expect($error->getMessage())->toBe('Injected document audit failure');
|
|
}
|
|
expect(ReceiptDocument::query()->count())->toBe(0)->and(Storage::disk('receipt_documents')->allFiles())->toBe([]);
|
|
app()->forgetInstance(RequisitionWorkflow::class);
|
|
$document = app(ReceiptDocuments::class)->add($this->f['receiver'], $this->receipt, receiptDocumentInput($this), $this->image);
|
|
expect($document->id)->toBeGreaterThan(0)->and(ReceiptDocument::query()->count())->toBe(1);
|
|
});
|
|
|
|
test('receipt document block leaves the established receipt page usable before installation', function () {
|
|
Schema::drop('receipt_documents');
|
|
$this->get($this->url)->assertOk()->assertInertia(fn (Assert $p) => $p->where('documentsReady', false)->where('canAddDocuments', false)->has('documents', 0));
|
|
$this->post($this->url.'/documentos', receiptDocumentInput($this))->assertStatus(503);
|
|
expect(GoodsReceipt::query()->whereKey($this->receipt->id)->exists())->toBeTrue();
|
|
});
|