141 lines
8.0 KiB
PHP
141 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Console\Commands\ImportDocumentBatch;
|
|
use App\Jobs\ScanDocumentForMalware;
|
|
use App\Models\User;
|
|
use App\Services\Malware\MalwareScanner;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Bus;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Support\SyntheticDocument;
|
|
use Tests\TestCase;
|
|
|
|
class ImportDocumentBatchTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private string $staging;
|
|
|
|
private string $batchName;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->batchName = 'test-'.Str::uuid();
|
|
$this->staging = storage_path('app/private/document-import-staging/'.$this->batchName);
|
|
File::ensureDirectoryExists($this->staging);
|
|
Storage::fake('local');
|
|
Bus::fake();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
File::deleteDirectory($this->staging);
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_preview_is_read_only_and_batch_import_is_private_idempotent_and_attributed(): void
|
|
{
|
|
$pdf = "%PDF-1.4\nfixture document\n%%EOF";
|
|
file_put_contents($this->staging.'/source.pdf', $pdf);
|
|
$record = [
|
|
'external_id' => 'test-document-v1', 'relative_path' => $this->batchName.'/source.pdf',
|
|
'title' => 'Documento de teste', 'issuer' => 'Entidade de teste', 'file_name' => 'documento.pdf',
|
|
'source_url' => 'https://example.test/documento.pdf', 'source_reference' => 'URL de teste; sem validação legal.',
|
|
'document_number' => 'TEST-1', 'document_date' => '2026-01-01', 'language' => 'pt',
|
|
'version_label' => 'edição de teste', 'category' => 'general', 'topic' => 'test', 'document_type' => 'guide',
|
|
];
|
|
file_put_contents($this->staging.'/manifest.json', json_encode(['documents' => [$record, $record]], JSON_THROW_ON_ERROR));
|
|
$actor = User::factory()->create();
|
|
config(['portal.managers' => [$actor->email]]);
|
|
|
|
$this->artisan(new ImportDocumentBatch, ['manifest' => $this->batchName.'/manifest.json'])->assertSuccessful();
|
|
$this->assertDatabaseCount('documents', 0);
|
|
$this->assertDatabaseCount('portal_resources', 0);
|
|
$this->assertSame([], Storage::disk('local')->allFiles('portal-documents'));
|
|
|
|
$this->artisan(new ImportDocumentBatch, ['manifest' => $this->batchName.'/manifest.json', '--apply' => true, '--actor' => (string) $actor->id])->assertSuccessful();
|
|
$this->assertDatabaseCount('documents', 1);
|
|
$this->assertDatabaseCount('portal_resources', 1);
|
|
$document = DB::table('documents')->firstOrFail();
|
|
$this->assertSame($actor->id, $document->uploaded_by);
|
|
$this->assertSame('quarantined', $document->status);
|
|
$this->assertSame('internal', $document->data_classification);
|
|
$this->assertSame('pending', $document->malware_scan_status);
|
|
$this->assertDatabaseHas('portal_resources', ['published' => false, 'rights_status' => 'unconfirmed', 'created_by' => $actor->id]);
|
|
$this->assertDatabaseHas('audit_events', ['actor_user_id' => $actor->id, 'action' => 'private_document_batch_imported']);
|
|
Bus::assertDispatchedTimes(ScanDocumentForMalware::class, 1);
|
|
|
|
$this->artisan(new ImportDocumentBatch, ['manifest' => $this->batchName.'/manifest.json', '--apply' => true, '--actor' => (string) $actor->id])->assertSuccessful();
|
|
$this->assertDatabaseCount('documents', 1);
|
|
$this->assertDatabaseCount('portal_resources', 1);
|
|
$this->assertDatabaseCount('audit_events', 1);
|
|
Bus::assertDispatchedTimes(ScanDocumentForMalware::class, 1);
|
|
}
|
|
|
|
public function test_apply_refuses_missing_actor_and_source_path_traversal(): void
|
|
{
|
|
file_put_contents($this->staging.'/source.pdf', "%PDF-1.4\nfixture\n%%EOF");
|
|
$record = ['external_id' => 'test-document', 'relative_path' => '..\\..\\..\\..\\.env', 'title' => 'Test', 'issuer' => 'Test'];
|
|
file_put_contents($this->staging.'/manifest.json', json_encode(['documents' => [$record]], JSON_THROW_ON_ERROR));
|
|
|
|
$this->artisan(new ImportDocumentBatch, ['manifest' => $this->batchName.'/manifest.json', '--apply' => true])->assertFailed();
|
|
$this->assertDatabaseCount('documents', 0);
|
|
|
|
$this->artisan(new ImportDocumentBatch, ['manifest' => $this->batchName.'/manifest.json'])->assertFailed();
|
|
$this->assertDatabaseCount('documents', 0);
|
|
}
|
|
|
|
public function test_qa_batch_real_scan_preserves_original_replay_identity_conflict_and_profile_guards(): void
|
|
{
|
|
config(['services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
|
|
$scanner = app(MalwareScanner::class);
|
|
if (! $scanner->health()['available']) {
|
|
$this->markTestSkipped('Real ClamD required for batch evidence.');
|
|
}
|
|
$bytes = SyntheticDocument::pdf('Synthetic QA batch - no official or legal value');
|
|
file_put_contents($this->staging.'/source.pdf', $bytes);
|
|
$record = ['external_id' => 'qa-batch-v1', 'relative_path' => $this->batchName.'/source.pdf', 'title' => 'Synthetic QA batch',
|
|
'issuer' => 'Synthetic QA issuer', 'source_url' => 'https://example.invalid/qa', 'version_label' => 'QA-1'];
|
|
file_put_contents($this->staging.'/manifest.json', json_encode(['documents' => [$record, $record]], JSON_THROW_ON_ERROR));
|
|
$actor = User::factory()->create();
|
|
$options = ['manifest' => $this->batchName.'/manifest.json', '--apply' => true, '--actor' => (string) $actor->id];
|
|
$this->artisan(new ImportDocumentBatch, $options)->assertFailed();
|
|
$this->assertDatabaseCount('documents', 0);
|
|
$this->assertSame([], Storage::disk('local')->allFiles('portal-documents'));
|
|
config(['portal.managers' => [$actor->email]]);
|
|
$this->app->detectEnvironment(fn () => 'production');
|
|
$this->artisan(new ImportDocumentBatch, $options)->assertFailed();
|
|
$this->app->detectEnvironment(fn () => 'testing');
|
|
$this->assertDatabaseCount('documents', 0);
|
|
$this->artisan(new ImportDocumentBatch, $options)->assertSuccessful();
|
|
$document = DB::table('documents')->firstOrFail();
|
|
$this->assertSame($bytes, Storage::disk('local')->get($document->storage_key));
|
|
$this->assertDatabaseCount('documents', 1);
|
|
$this->assertDatabaseCount('portal_resources', 1);
|
|
(new ScanDocumentForMalware($document->id))->handle($scanner);
|
|
$scanned = DB::table('documents')->where('id', $document->id)->first();
|
|
$this->assertSame('clean', $scanned->malware_scan_status);
|
|
$this->assertSame('ClamAV', $scanned->malware_engine);
|
|
$this->assertSame(hash('sha256', $bytes), $scanned->malware_scanned_sha256);
|
|
$response = $this->actingAs($actor)->get('/management/documents/'.$document->id.'/download')->assertOk();
|
|
$this->assertSame($bytes, file_get_contents($response->baseResponse->getFile()->getPathname()));
|
|
$resource = DB::table('portal_resources')->first();
|
|
$this->get('/documentos/'.$resource->id.'/download')->assertNotFound();
|
|
$this->artisan(new ImportDocumentBatch, $options)->assertSuccessful();
|
|
file_put_contents($this->staging.'/source.pdf', SyntheticDocument::pdf('Different QA bytes - same identity'));
|
|
$this->artisan(new ImportDocumentBatch, $options)->assertSuccessful();
|
|
$this->assertDatabaseCount('documents', 1);
|
|
$this->assertDatabaseCount('portal_resources', 1);
|
|
$this->assertSame($bytes, Storage::disk('local')->get($document->storage_key));
|
|
$this->assertDatabaseHas('portal_resources', ['id' => $resource->id, 'published' => false]);
|
|
$this->assertSame(1, DB::table('audit_events')->where('action', 'private_document_batch_imported')->count());
|
|
$this->actingAs(User::factory()->create())->get('/management/documents/'.$document->id.'/download')->assertForbidden();
|
|
}
|
|
}
|