'; } 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); } }