506 lines
20 KiB
PHP
506 lines
20 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Category;
|
|
use App\Models\Order;
|
|
use App\Models\Product;
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use App\Services\PriceCalculatorService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
class AdminContentTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $admin;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->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_3d_print_price_calculator_service_calculates_correctly(): void
|
|
{
|
|
$calculator = app(\App\Services\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'));
|
|
}
|
|
}
|