423 lines
23 KiB
PHP
423 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Jobs\ScanDocumentForMalware;
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use App\Services\Malware\MalwareScanner;
|
|
use App\Services\OpportunityManagement;
|
|
use App\Services\OpportunityTechnicalIncidents;
|
|
use App\Services\PortalProfileAdministration;
|
|
use Illuminate\Contracts\Encryption\DecryptException;
|
|
use Illuminate\Contracts\Queue\Job as QueueJob;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Bus;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
use Mockery;
|
|
use Tests\Support\CleanScannerFixture;
|
|
use Tests\TestCase;
|
|
|
|
final class ScanTechnicalIncidentsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $actor;
|
|
|
|
private User $support;
|
|
|
|
private Opportunity $opportunity;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
Http::fake();
|
|
Mail::fake();
|
|
Queue::fake();
|
|
Storage::fake('local');
|
|
config(['portal.profile_access_enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
|
|
$this->actor = User::factory()->create();
|
|
$this->support = User::factory()->create();
|
|
$approver = User::factory()->create();
|
|
$organization = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $organization, 'name' => 'SYNTHETIC upload QA organization', 'organization_type' => 'private']);
|
|
DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'user_id' => $this->actor->id, 'organization_id' => $organization, 'role' => 'officer']);
|
|
foreach ([[$this->actor, 'promoter'], [$this->support, 'technical_support']] as [$user,$role]) {
|
|
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $approver, ['user_id' => $user->id, 'role' => $role, 'organization_id' => $organization, 'opportunity_types' => ['investment_project'], 'source' => 'synthetic-resource-incident-fixture']);
|
|
}
|
|
$id = (string) Str::uuid();
|
|
$this->opportunity = new Opportunity(['reference' => $id, 'slug' => $id, 'title' => 'PRIVATE SYNTHETIC upload investment', 'status' => 'draft']);
|
|
$this->opportunity->forceFill(['owner_user_id' => $this->actor->id, 'organization_id' => $organization, 'opportunity_category' => 'investment_project', 'management_version' => 4, 'workflow_context' => ['working_state' => 'draft']])->save();
|
|
$this->actingAs($this->actor);
|
|
}
|
|
|
|
private function upload(): array
|
|
{
|
|
$input = ['etag' => app(OpportunityManagement::class)->etag($this->opportunity->fresh()), 'creation_key' => (string) Str::uuid(), 'reason' => 'PRIVATE SYNTHETIC scan upload reason', 'payload' => ['title' => 'PRIVATE SYNTHETIC scan original', 'classification' => 'confidential'], 'file' => UploadedFile::fake()->createWithContent('PRIVATE-synthetic.pdf', "%PDF-1.7\nSYNTHETIC scan bytes\n%%EOF")];
|
|
$id = $this->post('/minhas-oportunidades/'.$this->opportunity->id.'/resources/documents', $input, ['Accept' => 'application/json'])->assertCreated()->json('id');
|
|
$resource = DB::table('opportunity_documents')->where('id', $id)->first();
|
|
|
|
return [$resource, DB::table('documents')->where('id', $resource->document_id)->first()];
|
|
}
|
|
|
|
private function queued(): ScanDocumentForMalware
|
|
{
|
|
$captured = null;
|
|
Queue::assertPushed(ScanDocumentForMalware::class, function ($job) use (&$captured) {
|
|
$captured = $job;
|
|
|
|
return true;
|
|
});
|
|
$this->assertInstanceOf(ScanDocumentForMalware::class, $captured);
|
|
|
|
return $captured;
|
|
}
|
|
|
|
private function scannerResult(string $state = 'clean'): array
|
|
{
|
|
return ['result' => $state, 'reason_code' => match ($state) {
|
|
'clean' => null, 'infected' => 'threat_detected', default => 'scanner_unavailable'
|
|
}, 'engine' => 'QA-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => null, 'duration_ms' => 1, 'detail' => 'PRIVATE SYNTHETIC engine detail'];
|
|
}
|
|
|
|
private function scanner(\Closure $callback): MalwareScanner
|
|
{
|
|
return new class($callback) implements MalwareScanner
|
|
{
|
|
public function __construct(private \Closure $callback) {}
|
|
|
|
public function scan(string $path, int $fileSizeBytes, string $storageRoot): array
|
|
{
|
|
return ($this->callback)($path, $fileSizeBytes, $storageRoot);
|
|
}
|
|
|
|
public function health(): array
|
|
{
|
|
return ['available' => true, 'engine' => 'QA-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => null, 'signature_age_hours' => null, 'reason_code' => null];
|
|
}
|
|
};
|
|
}
|
|
|
|
private function roots(): int
|
|
{
|
|
return DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->count();
|
|
}
|
|
|
|
private function freshJob(ScanDocumentForMalware $job): ScanDocumentForMalware
|
|
{
|
|
return unserialize(serialize($job), ['allowed_classes' => [ScanDocumentForMalware::class]]);
|
|
}
|
|
|
|
private function delivery(int $attempt, string $uuid): QueueJob
|
|
{
|
|
$job = Mockery::mock(QueueJob::class);
|
|
$job->shouldReceive('attempts')->andReturn($attempt);
|
|
$job->shouldReceive('uuid')->andReturn($uuid);
|
|
|
|
return $job;
|
|
}
|
|
|
|
public function test_dispatch_failure_keeps_the_saved_resource_and_notifies_without_impersonating_the_initiator(): void
|
|
{
|
|
Bus::shouldReceive('dispatch')->once()->with(Mockery::type(ScanDocumentForMalware::class))->andThrow(new \RuntimeException('PRIVATE queue credentials and body'));
|
|
[$resource,$document] = $this->upload();
|
|
$this->assertSame(1, DB::table('documents')->count());
|
|
$this->assertSame(5, $this->opportunity->fresh()->management_version);
|
|
$this->assertSame('failed', $document->malware_scan_status);
|
|
$this->assertSame('quarantined', $document->status);
|
|
$this->assertStringContainsString('SYNTHETIC scan bytes', Crypt::decryptString(Storage::disk('local')->get($document->storage_key)));
|
|
$event = DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->first();
|
|
$data = json_decode($event->after_data, true)['incident'];
|
|
$this->assertNull($event->actor_user_id);
|
|
$this->assertSame($this->actor->id, $data['reported_for_user_id']);
|
|
$this->assertSame('scan_dispatch_unconfirmed', $data['error_code']);
|
|
$this->assertSame('scan_document', $data['operation']);
|
|
$this->assertSame(5, $data['observed_version']);
|
|
$this->assertStringNotContainsString('PRIVATE', $event->after_data);
|
|
$this->assertSame('scan_document', app(OpportunityTechnicalIncidents::class)->recent($this->actor, $this->opportunity->fresh())[0]['operation']);
|
|
$this->actingAs($this->support)->get('/suporte-tecnico/incidentes?unread=1')->assertOk()->assertInertia(fn (Assert $page) => $page->where('incidents.total', 1)->where('incidents.data.0.error_code', 'scan_dispatch_unconfirmed'));
|
|
$this->get('/minhas-oportunidades/'.$this->opportunity->id.'/resources')->assertForbidden();
|
|
}
|
|
|
|
public function test_an_uncertain_dispatch_after_a_sync_result_never_quarantines_an_already_clean_document(): void
|
|
{
|
|
Bus::shouldReceive('dispatch')->once()->with(Mockery::type(ScanDocumentForMalware::class))->andReturnUsing(function ($job) {
|
|
$job->handle(new CleanScannerFixture);
|
|
throw new \RuntimeException('PRIVATE dispatch acknowledgement lost');
|
|
});
|
|
[$resource,$document] = $this->upload();
|
|
$this->assertSame('clean', $document->malware_scan_status);
|
|
$this->assertSame('active', $document->status);
|
|
$this->assertSame(trim($document->sha256), $document->malware_scanned_sha256);
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
$this->assertSame(1, $this->roots());
|
|
$this->assertSame(5, $this->opportunity->fresh()->management_version);
|
|
}
|
|
|
|
public function test_dispatch_is_deferred_until_commit_and_a_rollback_never_enqueues_the_resource(): void
|
|
{
|
|
DB::beginTransaction();
|
|
$this->upload();
|
|
Queue::assertNothingPushed();
|
|
DB::rollBack();
|
|
$this->assertSame(0, DB::table('documents')->count());
|
|
$this->assertSame(0, $this->roots());
|
|
DB::beginTransaction();
|
|
$this->upload();
|
|
Queue::assertNothingPushed();
|
|
DB::commit();
|
|
$job = $this->queued();
|
|
$this->assertSame($this->actor->id, $job->opportunityContext['reported_for_user_id']);
|
|
$this->assertSame(5, $job->opportunityContext['observed_version']);
|
|
$this->assertSame(0, $this->roots());
|
|
}
|
|
|
|
public function test_inconclusive_scan_is_quarantined_has_one_system_incident_and_replay_does_not_rescan(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
$before = Storage::disk('local')->get($document->storage_key);
|
|
$business = $this->opportunity->fresh()->getRawOriginal();
|
|
$calls = 0;
|
|
$scanner = $this->scanner(function () use (&$calls) {
|
|
$calls++;
|
|
|
|
return $this->scannerResult('inconclusive');
|
|
});
|
|
$job->handle($scanner);
|
|
$this->freshJob($job)->handle($scanner);
|
|
$job->failed(new \RuntimeException('PRIVATE late failure'));
|
|
$current = DB::table('documents')->where('id', $document->id)->first();
|
|
$this->assertSame(1, $calls);
|
|
$this->assertSame('inconclusive', $current->malware_scan_status);
|
|
$this->assertSame('quarantined', $current->status);
|
|
$this->assertSame(1, $current->malware_scan_attempts);
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
$this->assertSame(1, $this->roots());
|
|
$this->assertSame($before, Storage::disk('local')->get($document->storage_key));
|
|
$this->assertSame([$document->storage_key], Storage::disk('local')->allFiles());
|
|
$this->assertSame($business, $this->opportunity->fresh()->getRawOriginal());
|
|
$root = DB::table('audit_events')->where('id', $job->runIdentifier())->first();
|
|
$data = json_decode($root->after_data, true)['incident'];
|
|
$this->assertNull($root->actor_user_id);
|
|
$this->assertSame('scan_inconclusive', $data['error_code']);
|
|
$this->assertStringNotContainsString('PRIVATE', $root->after_data);
|
|
app(OpportunityTechnicalIncidents::class)->recordScan($job->opportunityContext, $job->runIdentifier(), 'scan_worker_failed');
|
|
$this->assertEquals($root, DB::table('audit_events')->where('id', $job->runIdentifier())->first());
|
|
$this->get('/minhas-oportunidades/'.$this->opportunity->id.'/resources/documents/'.$resource->id.'/download')->assertStatus(423);
|
|
}
|
|
|
|
public function test_a_late_inconclusive_result_cannot_replace_a_newer_clean_scan(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$old = $this->queued();
|
|
$new = new ScanDocumentForMalware($document->id, $old->opportunityContext);
|
|
$old->handle($this->scanner(function () use ($new) {
|
|
$new->handle(new CleanScannerFixture);
|
|
|
|
return $this->scannerResult('inconclusive');
|
|
}));
|
|
$current = DB::table('documents')->where('id', $document->id)->first();
|
|
$this->assertSame('clean', $current->malware_scan_status);
|
|
$this->assertSame('active', $current->status);
|
|
$this->assertSame(2, $current->malware_scan_attempts);
|
|
$this->assertSame(trim($current->sha256), $current->malware_scanned_sha256);
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
$this->assertSame(0, $this->roots());
|
|
$this->assertSame(1, DB::table('audit_events')->where('action', 'scan_result_discarded')->where('correlation_id', $old->runIdentifier())->count());
|
|
$this->freshJob($old)->failed(new \RuntimeException('PRIVATE old failure'));
|
|
$this->assertEquals($current, DB::table('documents')->where('id', $document->id)->first());
|
|
}
|
|
|
|
public function test_same_queue_execution_different_deliveries_cannot_apply_an_old_failure_to_the_current_attempt(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$old = $this->queued();
|
|
$fresh = $this->freshJob($old);
|
|
$retry = $this->freshJob($old);
|
|
$uuid = (string) Str::uuid();
|
|
$old->setJob($this->delivery(1, $uuid));
|
|
$fresh->setJob($this->delivery(1, $uuid));
|
|
$retry->setJob($this->delivery(2, $uuid));
|
|
$old->handle($this->scanner(function () use ($retry, $fresh, $document) {
|
|
try {
|
|
$retry->handle($this->scanner(fn () => throw new \RuntimeException('PRIVATE current worker failed')));
|
|
} catch (\RuntimeException $error) {
|
|
$this->assertSame('PRIVATE current worker failed', $error->getMessage());
|
|
}
|
|
$fresh->failed(new \RuntimeException('PRIVATE previous delivery failure'));
|
|
$this->assertSame('scanning', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(0, $this->roots());
|
|
$retry->failed(new \RuntimeException('PRIVATE current terminal failure'));
|
|
|
|
return $this->scannerResult('clean');
|
|
}));
|
|
$this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(2, DB::table('documents')->where('id', $document->id)->value('malware_scan_attempts'));
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
$this->assertSame(1, $this->roots());
|
|
}
|
|
|
|
public function test_infected_result_and_its_original_are_preserved_against_late_callbacks_and_queued_jobs(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
$before = Storage::disk('local')->get($document->storage_key);
|
|
$job->handle($this->scanner(fn () => $this->scannerResult('infected')));
|
|
$current = DB::table('documents')->where('id', $document->id)->first();
|
|
$this->freshJob($job)->failed(new \RuntimeException('PRIVATE stale failure'));
|
|
(new ScanDocumentForMalware($document->id, $job->opportunityContext))->handle($this->scanner(fn () => throw new \RuntimeException('Scanner must not be called for infected bytes')));
|
|
$this->assertEquals($current, DB::table('documents')->where('id', $document->id)->first());
|
|
$this->assertSame('infected', $current->malware_scan_status);
|
|
$this->assertSame('quarantined', $current->status);
|
|
$this->assertSame($before, Storage::disk('local')->get($document->storage_key));
|
|
$this->assertSame(0, $this->roots());
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
}
|
|
|
|
public function test_document_record_changes_during_scan_are_inconclusive_and_history_keeps_the_attempted_hash(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
$before = Storage::disk('local')->get($document->storage_key);
|
|
$job->handle($this->scanner(function () use ($document) {
|
|
DB::table('documents')->where('id', $document->id)->update(['sha256' => str_repeat('b', 64)]);
|
|
|
|
return $this->scannerResult('clean');
|
|
}));
|
|
$current = DB::table('documents')->where('id', $document->id)->first();
|
|
$this->assertSame('inconclusive', $current->malware_scan_status);
|
|
$this->assertSame('document_record_changed_during_scan', $current->malware_failure_reason);
|
|
$this->assertNull($current->malware_scanned_sha256);
|
|
$this->assertSame(trim($document->sha256), DB::table('document_malware_scans')->value('file_sha256'));
|
|
$this->assertSame($before, Storage::disk('local')->get($document->storage_key));
|
|
$this->assertSame('scan_inconclusive', json_decode(DB::table('audit_events')->where('id', $job->runIdentifier())->value('after_data'), true)['incident']['error_code']);
|
|
}
|
|
|
|
public function test_fresh_serialized_failure_callback_owns_only_its_attempt_and_an_explicit_retry_remains_possible(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
$fresh = $this->freshJob($job);
|
|
try {
|
|
$job->handle($this->scanner(fn () => throw new \RuntimeException('PRIVATE engine failure')));
|
|
} catch (\RuntimeException $error) {
|
|
$this->assertSame('PRIVATE engine failure', $error->getMessage());
|
|
}
|
|
$fresh->failed(new \RuntimeException('PRIVATE terminal failure'));
|
|
$fresh->failed(new \RuntimeException('PRIVATE duplicate callback'));
|
|
$this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(1, $this->roots());
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
$fresh->handle(new CleanScannerFixture);
|
|
$this->assertSame('clean', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(2, DB::table('document_malware_scans')->count());
|
|
$this->assertSame(1, $this->roots());
|
|
}
|
|
|
|
public function test_legacy_serialized_payload_uses_the_real_queue_uuid_in_a_fresh_failure_callback(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$class = ScanDocumentForMalware::class;
|
|
$payload = 'O:'.strlen($class).':"'.$class.'":1:{s:10:"documentId";s:36:"'.$document->id.'";}';
|
|
$job = unserialize($payload, ['allowed_classes' => [$class]]);
|
|
$fresh = unserialize($payload, ['allowed_classes' => [$class]]);
|
|
$uuid = (string) Str::uuid();
|
|
$this->assertInstanceOf($class, $job);
|
|
$this->assertNull($job->executionId);
|
|
$job->setJob($this->delivery(1, $uuid));
|
|
$fresh->setJob($this->delivery(1, $uuid));
|
|
try {
|
|
$job->handle($this->scanner(fn () => throw new \RuntimeException('SYNTHETIC legacy worker failure')));
|
|
} catch (\RuntimeException) {
|
|
}
|
|
$fresh->failed(new \RuntimeException('SYNTHETIC legacy terminal callback'));
|
|
$this->assertSame($uuid, $fresh->runIdentifier());
|
|
$this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(0, $this->roots());
|
|
}
|
|
|
|
public function test_captured_organization_is_not_replaced_when_the_opportunity_moves_before_the_worker_runs(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
$captured = $job->opportunityContext;
|
|
$other = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $other, 'name' => 'SYNTHETIC other scan organization', 'organization_type' => 'private']);
|
|
$this->opportunity->forceFill(['organization_id' => $other])->save();
|
|
$job->handle($this->scanner(fn () => $this->scannerResult('inconclusive')));
|
|
$data = json_decode(DB::table('audit_events')->where('id', $job->runIdentifier())->value('after_data'), true)['incident'];
|
|
$this->assertSame($captured['organization_id'], $data['organization_id']);
|
|
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $this->actor, ['user_id' => $this->support->id, 'role' => 'technical_support', 'organization_id' => $other, 'opportunity_types' => ['investment_project']]);
|
|
$this->actingAs($this->support)->get('/suporte-tecnico/incidentes')->assertOk()->assertInertia(fn (Assert $page) => $page->where('incidents.total', 0)->where('profileAccess.technicalSupportUnread', 0));
|
|
$this->assertSame(1, $this->roots());
|
|
}
|
|
|
|
public function test_incident_persistence_failure_does_not_release_the_document_or_fail_a_completed_inconclusive_scan(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
DB::statement("ALTER TABLE audit_events ADD CONSTRAINT qa_reject_scan_incident CHECK (source_system <> 'portal-opportunity-incident')");
|
|
$job->handle($this->scanner(fn () => $this->scannerResult('inconclusive')));
|
|
$this->assertSame('quarantined', DB::table('documents')->where('id', $document->id)->value('status'));
|
|
$this->assertSame('inconclusive', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(1, DB::table('document_malware_scans')->count());
|
|
$this->assertSame(0, $this->roots());
|
|
}
|
|
|
|
public function test_generic_document_jobs_do_not_invent_an_opportunity_recipient(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = new ScanDocumentForMalware($document->id);
|
|
$job->handle($this->scanner(fn () => $this->scannerResult('inconclusive')));
|
|
$this->assertSame(0, $this->roots());
|
|
$this->assertSame('inconclusive', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
}
|
|
|
|
public function test_a_fresh_terminal_callback_preserves_the_attempted_byte_descriptor_after_a_record_change(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
$fresh = $this->freshJob($job);
|
|
try {
|
|
$job->handle($this->scanner(fn () => throw new \RuntimeException('SYNTHETIC worker interrupted')));
|
|
} catch (\RuntimeException) {
|
|
}
|
|
DB::table('documents')->where('id', $document->id)->update(['sha256' => str_repeat('b', 64), 'size_bytes' => 999]);
|
|
$fresh->failed(new \RuntimeException('SYNTHETIC terminal callback after record change'));
|
|
$result = DB::table('document_malware_scans')->first();
|
|
$this->assertSame(trim($document->sha256), $result->file_sha256);
|
|
$this->assertSame($document->size_bytes, $result->file_size_bytes);
|
|
$this->assertSame('inconclusive', $result->result);
|
|
$this->assertSame('document_record_changed_during_scan', $result->reason_code);
|
|
$this->assertSame('quarantined', DB::table('documents')->where('id', $document->id)->value('status'));
|
|
$this->assertSame('unknown', $result->engine);
|
|
}
|
|
|
|
public function test_failed_decryption_cleans_its_temporary_file_and_retains_the_original_and_attempt_context(): void
|
|
{
|
|
[$resource,$document] = $this->upload();
|
|
$job = $this->queued();
|
|
Storage::disk('local')->put($document->storage_key, 'PRIVATE SYNTHETIC invalid envelope');
|
|
try {
|
|
$job->handle(new CleanScannerFixture);
|
|
$this->fail('Invalid envelope must not be scanned as clean.');
|
|
} catch (DecryptException) {
|
|
}
|
|
$this->freshJob($job)->failed(new \RuntimeException('PRIVATE terminal decryption failure'));
|
|
$this->assertSame([$document->storage_key], Storage::disk('local')->allFiles());
|
|
$this->assertSame('PRIVATE SYNTHETIC invalid envelope', Storage::disk('local')->get($document->storage_key));
|
|
$this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status'));
|
|
$this->assertSame(1, $this->roots());
|
|
}
|
|
}
|