admin = User::create([ 'name' => 'Admin Jouwdriedee', 'email' => 'info@jouwdriedee.nl', 'password' => bcrypt('Jw3D!x9#QmK7$vR2'), 'is_admin' => true, ]); } public function test_guest_is_redirected_to_login_when_accessing_admin(): void { $response = $this->get(route('admin.content.index')); $response->assertRedirect(route('login')); } public function test_regular_customer_cannot_access_admin(): void { $customer = User::create([ 'name' => 'Klant Piet', 'email' => 'piet@example.com', 'password' => bcrypt('secret1234'), 'is_admin' => false, ]); $response = $this->actingAs($customer)->get(route('admin.content.index')); $response->assertRedirect(route('home')); $response->assertSessionHas('error'); } public function test_admin_content_editor_loads_successfully(): void { $response = $this->actingAs($this->admin)->get(route('admin.content.index')); $response->assertStatus(200); $response->assertSee('Homepage Hero'); $response->assertSee('Shop Content & Stijl CMS', false); } public function test_admin_can_update_layout_and_texts(): void { $payload = [ 'site_name' => 'Mega 3D Shop', 'hero_title' => 'Custom 3D Speelgoed & Figuren', 'hero_subtitle' => 'Nieuwe custom introductietekst voor de winkel.', 'hero_primary_btn_text' => 'Direct Winkelen', 'usp_title' => 'Waarom kiezen voor onze prints?', 'footer_email' => 'hallo@mega3d.nl', 'announcement_enabled' => '1', 'announcement_text' => 'Tijdelijke actie: gratis verzending!', ]; $response = $this->actingAs($this->admin)->post(route('admin.content.update'), $payload); $response->assertRedirect(route('admin.content.index')); $response->assertSessionHas('success'); $this->assertSame('Mega 3D Shop', Setting::get('site_name')); $this->assertSame('Custom 3D Speelgoed & Figuren', Setting::get('hero_title')); $this->assertSame('hallo@mega3d.nl', Setting::get('footer_email')); // Homepage should now display the updated content! $homeResponse = $this->get(route('home')); $homeResponse->assertStatus(200); $homeResponse->assertSee('Custom 3D Speelgoed & Figuren'); $homeResponse->assertSee('Nieuwe custom introductietekst voor de winkel.'); $homeResponse->assertSee('Direct Winkelen'); $homeResponse->assertSee('Waarom kiezen voor onze prints?'); $homeResponse->assertSee('hallo@mega3d.nl'); $homeResponse->assertSee('Tijdelijke actie: gratis verzending!'); } public function test_admin_can_reset_content_to_defaults(): void { Setting::set('hero_title', 'Tijdelijke Titel'); $this->assertSame('Tijdelijke Titel', Setting::get('hero_title')); $response = $this->actingAs($this->admin)->post(route('admin.content.reset')); $response->assertRedirect(route('admin.content.index')); $this->assertNull(Setting::get('hero_title')); } public function test_admin_can_view_and_edit_products(): void { $category = Category::create([ 'name' => 'Fidgets', 'slug' => 'fidgets', ]); $product = Product::create([ 'name' => 'Test Fidget Draak', 'slug' => 'test-fidget-draak', 'category_id' => $category->id, 'base_price' => 12.50, 'short_description' => 'Korte test', 'description' => 'Uitgebreide test beschrijving', 'is_active' => true, ]); // Products index $indexResponse = $this->actingAs($this->admin)->get(route('admin.products.index')); $indexResponse->assertStatus(200); $indexResponse->assertSee('Test Fidget Draak'); // Edit page $editResponse = $this->actingAs($this->admin)->get(route('admin.products.edit', $product)); $editResponse->assertStatus(200); $editResponse->assertSee('Test Fidget Draak'); // Update product $updateResponse = $this->actingAs($this->admin)->put(route('admin.products.update', $product), [ 'name' => 'Aangepaste Fidget Draak', 'category_id' => $category->id, 'base_price' => 18.00, 'short_description' => 'Geüpdatete korte test', 'description' => 'Nieuwe lange beschrijving', 'is_active' => 1, 'is_featured' => 1, ]); $updateResponse->assertRedirect(route('admin.products.index')); $this->assertDatabaseHas('products', [ 'id' => $product->id, 'name' => 'Aangepaste Fidget Draak', 'base_price' => 18.00, ]); } public function test_admin_can_view_orders(): void { $order = Order::create([ 'order_number' => 'ORD-TEST-999', 'customer_name' => 'Jan Jansen', 'customer_email' => 'jan@example.com', 'shipping_street' => 'Dorpstraat', 'shipping_number' => '1', 'shipping_postal_code' => '1234AB', 'shipping_city' => 'Amsterdam', 'subtotal' => 25.00, 'shipping_cost' => 0.00, 'total' => 25.00, 'status' => 'pending', ]); $response = $this->actingAs($this->admin)->get(route('admin.orders.index')); $response->assertStatus(200); $response->assertSee('ORD-TEST-999'); $response->assertSee('Jan Jansen'); $showResponse = $this->actingAs($this->admin)->get(route('admin.orders.show', $order)); $showResponse->assertStatus(200); $showResponse->assertSee('ORD-TEST-999'); } public function test_admin_can_update_product_images_and_gallery(): void { $category = Category::create([ 'name' => 'Fidget Gadgets', 'slug' => 'fidget-gadgets-gallery-test', ]); $product = Product::create([ 'category_id' => $category->id, 'name' => 'Multi Foto Draak', 'slug' => 'multi-foto-draak', 'short_description' => 'Test met meerdere fotos', 'description' => 'Test beschrijving met galerij', 'base_price' => 19.95, 'image_url' => 'https://cdn.example.com/cover.jpg', 'gallery_images' => ['https://cdn.example.com/photo1.jpg', 'https://cdn.example.com/photo2.jpg'], 'is_active' => true, ]); // Edit page loads with visual image gallery $editResponse = $this->actingAs($this->admin)->get(route('admin.products.edit', $product)); $editResponse->assertStatus(200); $editResponse->assertSee('Productafbeeldingen & Fotogalerij', false); $editResponse->assertSee('Hoofdafbeelding (Cover)'); // Update product with new cover and extra gallery images $updateResponse = $this->actingAs($this->admin)->put(route('admin.products.update', $product), [ 'name' => 'Multi Foto Draak Bijgewerkt', 'category_id' => $category->id, 'base_price' => 21.50, 'short_description' => 'Geüpdatet', 'description' => 'Nieuwe omschrijving', 'image_url' => 'https://cdn.example.com/new-cover.jpg', 'gallery_images' => [ 'https://cdn.example.com/photo1.jpg', 'https://cdn.example.com/photo2.jpg', 'https://cdn.example.com/photo3.jpg', 'https://cdn.example.com/new-cover.jpg', // Should be excluded because it matches cover ], 'is_active' => 1, ]); $updateResponse->assertRedirect(route('admin.products.index')); $product->refresh(); $this->assertEquals('https://cdn.example.com/new-cover.jpg', $product->image_url); $this->assertCount(3, $product->gallery_images); $this->assertContains('https://cdn.example.com/photo3.jpg', $product->gallery_images); $this->assertNotContains('https://cdn.example.com/new-cover.jpg', $product->gallery_images); // Storefront product details page shows the multi-image gallery $shopResponse = $this->get(route('product.show', $product->slug)); $shopResponse->assertStatus(200); $shopResponse->assertSee('https://cdn.example.com/new-cover.jpg'); $shopResponse->assertSee('https://cdn.example.com/photo1.jpg'); $shopResponse->assertSee('https://cdn.example.com/photo3.jpg'); } public function test_admin_can_upload_product_image(): void { Storage::fake('public'); $onePixelJpeg = base64_decode('/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA='); $file = UploadedFile::fake()->createWithContent('test-print-photo.jpg', $onePixelJpeg); $response = $this->actingAs($this->admin)->postJson(route('admin.products.upload-image'), [ 'image' => $file, ]); $response->assertStatus(200); $response->assertJson([ 'success' => true, ]); $url = $response->json('url'); $this->assertStringStartsWith('/storage/products/', $url); $storagePath = str_replace('/storage/', '', $url); Storage::disk('public')->assertExists($storagePath); } public function test_admin_can_toggle_webshop_offline_mode(): void { $this->assertEquals('0', Setting::get('shop_offline', '0')); // Toggle offline via admin button $response = $this->actingAs($this->admin)->post(route('admin.toggle-offline')); $response->assertRedirect(); Setting::flushCache(); $this->assertEquals('1', Setting::get('shop_offline')); // Logout admin so subsequent requests are unauthenticated guest requests auth()->logout(); $this->flushSession(); // Guest visitor receives 503 with big red offline banner $guestResponse = $this->get(route('home')); $guestResponse->assertStatus(503); $guestResponse->assertSee('Aan deze webshop wordt hard gewerkt', false); $guestResponse->assertSee('Webshop Tijdelijk Offline'); // Other customer routes are blocked $catalogResponse = $this->get(route('catalog')); $catalogResponse->assertStatus(503); // Login page remains accessible $loginResponse = $this->get(route('login')); $loginResponse->assertStatus(200); // Admin can still view storefront normally with preview banner $adminShopResponse = $this->actingAs($this->admin)->get(route('home')); $adminShopResponse->assertStatus(200); $adminShopResponse->assertSee('OFFLINE BEHEERDERS-PREVIEW'); // Toggle back online $this->actingAs($this->admin)->post(route('admin.toggle-offline')); Setting::flushCache(); $this->assertEquals('0', Setting::get('shop_offline')); // Logout admin and verify guest can visit normally again auth()->logout(); $this->flushSession(); $guestOnlineResponse = $this->get(route('home')); $guestOnlineResponse->assertStatus(200); } public function test_site_sends_modern_security_headers(): void { $response = $this->get('/'); $response->assertHeader('X-Frame-Options', 'SAMEORIGIN'); $response->assertHeader('X-Content-Type-Options', 'nosniff'); $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); } public function test_3d_print_price_calculator_service_calculates_correctly(): void { $calculator = app(PriceCalculatorService::class); // 100 grams, 4 hours, 1 color, PLA, 40% margin $result = $calculator->calculate( weightGrams: 100, printTimeHours: 4.0, colorCount: 1, material: 'pla', customMarkupPercent: 40.0 ); $this->assertArrayHasKey('suggested_price', $result); $this->assertArrayHasKey('suggested_price_formatted', $result); $this->assertArrayHasKey('breakdown', $result); $this->assertArrayHasKey('material_cost', $result['breakdown']); $this->assertArrayHasKey('electricity_cost', $result['breakdown']); $this->assertArrayHasKey('machine_cost', $result['breakdown']); $this->assertArrayHasKey('total_cost_price', $result['breakdown']); $this->assertGreaterThan(5.0, $result['suggested_price']); // Multi-color test: 4 colors should cost more than 1 color due to purge waste & setup $multiColorResult = $calculator->calculate( weightGrams: 100, printTimeHours: 4.0, colorCount: 4, material: 'pla', customMarkupPercent: 40.0 ); $this->assertGreaterThan($result['suggested_price'], $multiColorResult['suggested_price']); $this->assertGreaterThan($result['breakdown']['material_cost'], $multiColorResult['breakdown']['material_cost']); } public function test_admin_can_use_calculate_price_api_endpoint(): void { $response = $this->actingAs($this->admin)->postJson(route('admin.products.calculate-price'), [ 'weight_grams' => 120, 'print_time_hours' => 5.5, 'color_count' => 2, 'material' => 'petg', 'markup_percent' => 50, ]); $response->assertStatus(200); $response->assertJsonStructure([ 'success', 'data' => [ 'inputs', 'rates', 'breakdown' => [ 'effective_weight_grams', 'material_cost', 'electricity_cost', 'machine_cost', 'handling_cost', 'failure_cost', 'total_cost_price', 'profit_amount', 'price_excl_vat', 'vat_amount', 'price_incl_vat_exact', ], 'suggested_price', 'suggested_price_formatted', ], ]); } public function test_admin_can_save_calculator_tariffs_in_content_settings(): void { $response = $this->actingAs($this->admin)->post(route('admin.content.update'), [ 'calc_filament_price_kg' => '22.50', 'calc_electricity_kwh' => '0.40', 'calc_printer_wattage' => '180', 'calc_machine_hourly_rate' => '0.65', 'calc_handling_flat' => '2.00', 'calc_default_markup' => '45', 'calc_failure_rate' => '8', ]); $response->assertRedirect(route('admin.content.index')); Setting::flushCache(); $this->assertEquals(22.50, (float) Setting::get('calc_filament_price_kg')); $this->assertEquals(0.40, (float) Setting::get('calc_electricity_kwh')); $this->assertEquals(180, (int) Setting::get('calc_printer_wattage')); $this->assertEquals(0.65, (float) Setting::get('calc_machine_hourly_rate')); $this->assertEquals(2.00, (float) Setting::get('calc_handling_flat')); $this->assertEquals(45, (int) Setting::get('calc_default_markup')); $this->assertEquals(8, (int) Setting::get('calc_failure_rate')); } public function test_sending_test_mail_redirects_and_stays_on_email_tab(): void { Mail::fake(); $response = $this->actingAs($this->admin)->post(route('admin.content.test-mail'), [ 'test_email' => 'niek.rabelink@gmail.com', ]); $response->assertRedirect(route('admin.content.index', ['tab' => 'email'])); $response->assertSessionHas('success'); } public function test_updating_content_with_active_tab_preserves_tab(): void { $response = $this->actingAs($this->admin)->post(route('admin.content.update'), [ 'active_tab' => 'calculator', 'site_name' => 'Jouwdriedee 3D Print Studio', ]); $response->assertRedirect(route('admin.content.index', ['tab' => 'calculator'])); $response->assertSessionHas('success'); } }