97 lines
5.6 KiB
PHP
97 lines
5.6 KiB
PHP
<?php
|
|
|
|
use App\Models\ReceiptDocument;
|
|
use App\Services\ReceiptDocumentHealth;
|
|
use App\Services\ReceiptDocuments;
|
|
use App\Services\ReceiptWorkflow;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
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 Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
require_once __DIR__.'/../Support/ReceiptFixture.php';
|
|
|
|
beforeEach(function () {
|
|
$this->directory = storage_path('framework/testing/receipt-health-'.Str::uuid());
|
|
File::ensureDirectoryExists($this->directory);
|
|
Storage::set('receipt_documents', Storage::build(['driver' => 'local', 'root' => $this->directory, 'throw' => true]));
|
|
$this->f = receiptFixture();
|
|
$this->actingAs($this->f['receiver']);
|
|
$this->receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f));
|
|
$this->bytes = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6E4sAAAAASUVORK5CYII=');
|
|
$this->document = app(ReceiptDocuments::class)->add($this->f['receiver'], $this->receipt, ['operation_key' => (string) Str::uuid(), 'document_type' => 'delivery_note'], UploadedFile::fake()->createWithContent('private-name.png', $this->bytes));
|
|
auth()->logout();
|
|
});
|
|
|
|
afterEach(function () {
|
|
File::deleteDirectory($this->directory);
|
|
});
|
|
|
|
test('receipt document health checks native evidence without mutations or disclosing private identifiers', function () {
|
|
$other = receiptFixture();
|
|
$this->actingAs($other['receiver']);
|
|
$receipt = app(ReceiptWorkflow::class)->confirm($other['receiver'], $other['order'], receiptPayload($other));
|
|
app(ReceiptDocuments::class)->add($other['receiver'], $receipt, ['operation_key' => (string) Str::uuid(), 'document_type' => 'quality'], UploadedFile::fake()->createWithContent('other-private.png', $this->bytes));
|
|
auth()->logout();
|
|
$before = [];
|
|
foreach (['receipt_documents', 'audit_logs', 'goods_receipts', 'stock_movements', 'approval_steps'] as $table) {
|
|
$before[$table] = DB::table($table)->orderBy('id')->get()->toArray();
|
|
}
|
|
expect(Artisan::call('sgc:receipt-documents-check', ['--json' => true]))->toBe(0);
|
|
$output = Artisan::output();
|
|
$result = json_decode($output, true, 32, JSON_THROW_ON_ERROR);
|
|
expect($result['ok'])->toBeTrue()->and($result['complete'])->toBeTrue()->and($result['documents_checked'])->toBe(2)->and($result['files_checked'])->toBe(2);
|
|
expect($output)->not->toContain($this->directory, $this->document->path, 'private-name.png');
|
|
foreach ($before as $table => $rows) {
|
|
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows);
|
|
}
|
|
expect(Storage::disk('receipt_documents')->get($this->document->path))->toBe($this->bytes);
|
|
});
|
|
|
|
test('receipt document health detects missing altered and unreferenced files without removing any', function () {
|
|
$disk = Storage::disk('receipt_documents');
|
|
$disk->delete($this->document->path);
|
|
expect(app(ReceiptDocumentHealth::class)->inspect()['missing'])->toBe(1);
|
|
$changed = $this->bytes;
|
|
$changed[10] = 'x';
|
|
$disk->put($this->document->path, $changed);
|
|
$disk->put('recent.bin', 'recent');
|
|
$disk->put('older.bin', 'older');
|
|
touch($disk->path('older.bin'), now()->subHours(2)->timestamp);
|
|
$result = app(ReceiptDocumentHealth::class)->inspect();
|
|
expect($result['ok'])->toBeFalse()->and($result['altered'])->toBe(1)->and($result['unreferenced_recent'])->toBe(1)->and($result['unreferenced_older'])->toBe(1);
|
|
expect($disk->allFiles())->toHaveCount(3)->and($disk->get($this->document->path))->toBe($changed);
|
|
expect(ReceiptDocument::withoutGlobalScopes()->count())->toBe(1);
|
|
});
|
|
|
|
test('receipt document health rejects invalid paths and reports a bounded incomplete scan', function () {
|
|
DB::table('receipt_documents')->insert([...$this->document->getRawOriginal(), 'id' => $this->document->id + 1, 'document_type' => 'other', 'path' => '../outside-secret.txt']);
|
|
expect(app(ReceiptDocumentHealth::class)->inspect()['invalid_reference'])->toBe(1);
|
|
$result = app(ReceiptDocumentHealth::class)->inspect(1);
|
|
expect($result['ok'])->toBeFalse()->and($result['complete'])->toBeFalse()->and($result['reasons'])->toContain('limit_reached');
|
|
expect(Artisan::call('sgc:receipt-documents-check', ['--limit' => '10001']))->toBe(2);
|
|
});
|
|
|
|
test('receipt document health distinguishes unavailable installation and hides technical errors', function () {
|
|
Schema::drop('receipt_documents');
|
|
expect(app(ReceiptDocumentHealth::class)->inspect()['reasons'])->toBe(['not_installed']);
|
|
$this->mock(ReceiptDocumentHealth::class)->shouldReceive('inspect')->andThrow(new RuntimeException('private secret path/password'));
|
|
expect(Artisan::call('sgc:receipt-documents-check', ['--json' => true]))->toBe(1);
|
|
expect(Artisan::output())->toContain('unavailable')->not->toContain('password', 'secret');
|
|
});
|
|
|
|
test('receipt document health refuses an interactive identity instead of exposing a global tenant scan', function () {
|
|
$this->actingAs($this->f['receiver']);
|
|
expect(fn () => app(ReceiptDocumentHealth::class)->inspect())->toThrow(HttpException::class);
|
|
});
|
|
|
|
test('receipt document health does not certify an unavailable storage root', function () {
|
|
File::deleteDirectory($this->directory);
|
|
$result = app(ReceiptDocumentHealth::class)->inspect();
|
|
expect($result['ok'])->toBeFalse()->and($result['complete'])->toBeFalse()->and($result['reasons'])->toBe(['storage_unavailable']);
|
|
});
|