';
}
private function validThreeMfBytes(): string
{
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
$zip = new ZipArchive;
$zip->open($path, ZipArchive::CREATE);
$zip->addFromString('[Content_Types].xml', '');
$zip->addFromString('3D/3dmodel.model', $this->validModelXml());
$zip->addFromString('_rels/.rels', '');
$zip->close();
$bytes = file_get_contents($path);
unlink($path);
return $bytes;
}
private function maliciousThreeMfBytes(): string
{
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
$zip = new ZipArchive;
$zip->open($path, ZipArchive::CREATE);
$zip->addFromString('[Content_Types].xml', '');
$zip->addFromString('3D/3dmodel.model', $this->validModelXml());
$zip->addFromString('bomb.bin', str_repeat("\0", 20 * 1024 * 1024));
$zip->setCompressionName('bomb.bin', ZipArchive::CM_DEFLATE, 9);
$zip->close();
$bytes = file_get_contents($path);
unlink($path);
return $bytes;
}
/**
* @return array{0: Project, 1: ProjectFile}
*/
private function projectWith3mfFile(string $bytes): array
{
$project = Project::factory()->create();
$revision = ProjectRevision::factory()->for($project)->create();
$key = 'source.3mf';
Storage::disk('project_files')->put($key, $bytes);
$file = ProjectFile::factory()->for($revision, 'revision')->create([
'extension' => '3mf', 'media_type' => 'model/3mf', 'storage_key' => $key, 'original_filename' => 'widget.3mf',
]);
return [$project, $file];
}
public function test_generation_is_inside_the_serializing_database_transaction(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$scene = (new ThreeMfGeometryExtractor)->extract(Storage::disk('project_files')->path($file->storage_key));
$level = DB::transactionLevel();
$this->mock(ThreeMfGeometryExtractor::class)->shouldReceive('extract')->once()->andReturnUsing(function () use ($scene, $level): array {
$this->assertGreaterThan($level, DB::transactionLevel());
return $scene;
});
$this->actingAs(User::factory()->create(['role' => 'admin']))
->post(route('projects.files.render', [$project, $file]))->assertRedirect();
}
public function test_failed_render_storage_does_not_create_a_render_record(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$disk = \Mockery::mock(Storage::disk('project_renders'))->makePartial();
$disk->shouldReceive('put')->once()->andReturnFalse();
Storage::set('project_renders', $disk);
$this->actingAs(User::factory()->create(['role' => 'admin']))
->post(route('projects.files.render', [$project, $file]))->assertStatus(422);
$this->assertDatabaseCount('project_file_renders', 0);
}
public function test_audit_failure_rolls_back_the_record_and_cleans_up_generated_bytes(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
ProjectEvent::creating(function (): void {
throw new \RuntimeException('Synthetic audit storage failure');
});
try {
$this->actingAs(User::factory()->create(['role' => 'admin']))
->post(route('projects.files.render', [$project, $file]))->assertStatus(422);
$this->assertDatabaseCount('project_file_renders', 0);
$this->assertSame([], Storage::disk('project_renders')->allFiles());
} finally {
ProjectEvent::flushEventListeners();
}
}
public function test_admin_can_generate_a_product_image_from_a_valid_3mf_file(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]))
->assertRedirect(route('projects.show', $project));
$this->assertDatabaseCount('project_file_renders', 1);
$render = ProjectFileRender::first();
$this->assertSame($file->id, $render->project_file_id);
$this->assertSame(1, $render->triangle_count);
$this->assertSame(1, $render->instance_count);
$this->assertStringContainsString('widget.3mf', $render->alt_text);
Storage::disk('project_renders')->assertExists($render->storage_key);
$this->assertDatabaseHas('project_events', ['project_id' => $project->id, 'event_type' => 'product_image_generated']);
}
public function test_generating_does_not_modify_the_original_file(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
$admin = User::factory()->create(['role' => 'admin']);
$bytes = $this->validThreeMfBytes();
[$project, $file] = $this->projectWith3mfFile($bytes);
$before = Storage::disk('project_files')->get($file->storage_key);
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]));
$after = Storage::disk('project_files')->get($file->storage_key);
$this->assertSame($before, $after);
$this->assertSame(hash('sha256', $bytes), hash('sha256', $after));
}
public function test_calling_generate_twice_is_idempotent_and_creates_only_one_render(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]))->assertRedirect();
$firstKey = ProjectFileRender::first()->storage_key;
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]))
->assertRedirect(route('projects.show', $project))
->assertSessionHas('status', 'Product image already generated for this file.');
$this->assertDatabaseCount('project_file_renders', 1);
$this->assertSame($firstKey, ProjectFileRender::first()->storage_key);
$this->assertDatabaseCount('project_events', 1);
}
public function test_rejects_generation_for_a_non_3mf_file(): void
{
Storage::fake('project_files');
$admin = User::factory()->create(['role' => 'admin']);
$project = Project::factory()->create();
$revision = ProjectRevision::factory()->for($project)->create();
$file = ProjectFile::factory()->for($revision, 'revision')->create(['extension' => 'stl']);
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]))
->assertSessionHasErrors('file');
$this->assertDatabaseCount('project_file_renders', 0);
}
public function test_rejects_a_malicious_archive_with_a_clear_error_and_no_render_created(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->maliciousThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]))
->assertSessionHasErrors('file');
$this->assertDatabaseCount('project_file_renders', 0);
$this->assertCount(0, Storage::disk('project_renders')->allFiles());
}
public function test_guest_cannot_generate_preview_or_download(): void
{
Storage::fake('project_files');
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->post(route('projects.files.render', [$project, $file]))->assertRedirect(route('login'));
$this->get(route('projects.files.render.preview', [$project, $file]))->assertRedirect(route('login'));
$this->get(route('projects.files.render.download', [$project, $file]))->assertRedirect(route('login'));
}
public function test_non_admin_cannot_generate_preview_or_download(): void
{
Storage::fake('project_files');
$user = User::factory()->create();
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($user)->post(route('projects.files.render', [$project, $file]))->assertForbidden();
$this->actingAs($user)->get(route('projects.files.render.preview', [$project, $file]))->assertForbidden();
$this->actingAs($user)->get(route('projects.files.render.download', [$project, $file]))->assertForbidden();
}
public function test_cannot_reach_a_file_through_a_different_projects_url(): void
{
Storage::fake('project_files');
$admin = User::factory()->create(['role' => 'admin']);
$otherProject = Project::factory()->create();
[, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$otherProject, $file]))->assertNotFound();
$this->actingAs($admin)->get(route('projects.files.render.preview', [$otherProject, $file]))->assertNotFound();
$this->actingAs($admin)->get(route('projects.files.render.download', [$otherProject, $file]))->assertNotFound();
}
public function test_preview_and_download_404_before_a_render_exists(): void
{
Storage::fake('project_files');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->get(route('projects.files.render.preview', [$project, $file]))->assertNotFound();
$this->actingAs($admin)->get(route('projects.files.render.download', [$project, $file]))->assertNotFound();
}
public function test_preview_returns_the_image_with_the_correct_content_type(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]));
$render = ProjectFileRender::first();
$this->actingAs($admin)->get(route('projects.files.render.preview', [$project, $file]))
->assertOk()
->assertHeader('Content-Type', $render->mime_type);
}
public function test_download_returns_an_attachment_named_after_the_original_file(): void
{
Storage::fake('project_files');
Storage::fake('project_renders');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]));
$render = ProjectFileRender::first();
$extension = $render->mime_type === 'image/png' ? 'png' : 'svg';
$this->actingAs($admin)->get(route('projects.files.render.download', [$project, $file]))
->assertOk()
->assertHeader('content-disposition', 'attachment; filename='.$file->original_filename.'.'.$extension);
}
public function test_viewing_the_project_page_never_triggers_rendering(): void
{
Storage::fake('project_files');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk();
$this->assertDatabaseCount('project_file_renders', 0);
}
public function test_missing_csrf_token_is_rejected_when_verification_actually_runs(): void
{
$this->app['env'] = 'local';
Storage::fake('project_files');
$admin = User::factory()->create(['role' => 'admin']);
[$project, $file] = $this->projectWith3mfFile($this->validThreeMfBytes());
$this->actingAs($admin)->post(route('projects.files.render', [$project, $file]))
->assertStatus(419);
}
}