519 lines
16 KiB
PHP
519 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Enums\PriceType;
|
|
use App\Enums\ProductStatus;
|
|
use App\Enums\ProductType;
|
|
use App\Models\Category;
|
|
use App\Models\Hub;
|
|
use App\Models\Partner;
|
|
use App\Models\Product;
|
|
use App\Models\ProductActivity;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Volt\Volt;
|
|
use Spatie\Permission\Models\Role;
|
|
|
|
beforeEach(function () {
|
|
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
|
Role::create(['name' => 'Retailer']);
|
|
Role::create(['name' => 'Manufacturer']);
|
|
Role::create(['name' => 'Customer']);
|
|
Role::create(['name' => 'Admin']);
|
|
});
|
|
|
|
function createTeaserPartnerWithHub(): array
|
|
{
|
|
$hub = Hub::factory()->create();
|
|
$partner = Partner::factory()->setupCompleted()->create(['hub_id' => $hub->id]);
|
|
$user = User::factory()->create(['partner_id' => $partner->id]);
|
|
|
|
return [$user, $partner, $hub];
|
|
}
|
|
|
|
function createTeaserProduct(Partner $partner, array $overrides = []): Product
|
|
{
|
|
$category = Category::factory()->create();
|
|
|
|
$product = Product::factory()->create(array_merge([
|
|
'partner_id' => $partner->id,
|
|
'product_type' => ProductType::LocalStock,
|
|
'status' => ProductStatus::Pending,
|
|
'price_type' => PriceType::FromPrice,
|
|
'price_display_text' => 'Ab 2.500 €',
|
|
'name' => 'Teaser Produkt',
|
|
'description_short' => 'Kurzbeschreibung Teaser',
|
|
'is_available' => true,
|
|
'is_curated' => false,
|
|
], $overrides));
|
|
|
|
$product->categories()->attach($category->id);
|
|
|
|
return $product;
|
|
}
|
|
|
|
// --- Access Tests ---
|
|
|
|
test('owner can access teaser product edit page', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('products.edit.teaser', $product))
|
|
->assertSuccessful();
|
|
});
|
|
|
|
test('admin can access any teaser product edit page', function () {
|
|
[$user] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Admin');
|
|
$otherPartner = Partner::factory()->setupCompleted()->create();
|
|
$product = createTeaserProduct($otherPartner);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('products.edit.teaser', $product))
|
|
->assertSuccessful();
|
|
});
|
|
|
|
test('other partner cannot access teaser product edit page', function () {
|
|
[$user] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
|
|
$otherPartner = Partner::factory()->setupCompleted()->create();
|
|
$product = createTeaserProduct($otherPartner);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('products.edit.teaser', $product))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('customer cannot access teaser product edit page', function () {
|
|
$customer = User::factory()->create();
|
|
$customer->assignRole('Customer');
|
|
|
|
$partner = Partner::factory()->setupCompleted()->create();
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($customer)
|
|
->get(route('products.edit.teaser', $product))
|
|
->assertForbidden();
|
|
});
|
|
|
|
// --- Mount Pre-Fill Tests ---
|
|
|
|
test('teaser edit page pre-fills product data', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, [
|
|
'name' => 'Vorhandenes Sideboard',
|
|
'description_short' => 'Ein tolles Sideboard.',
|
|
'price_type' => PriceType::FromPrice,
|
|
'price_display_text' => 'Ab 3.000 €',
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('name', 'Vorhandenes Sideboard')
|
|
->assertSet('descriptionShort', 'Ein tolles Sideboard.')
|
|
->assertSet('priceType', 'from_price')
|
|
->assertSet('priceDisplayText', 'Ab 3.000 €')
|
|
->assertSet('partnerProductNumber', '');
|
|
});
|
|
|
|
test('teaser edit page pre-fills category', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
$expectedCategoryId = $product->categories->first()->id;
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('categoryId', $expectedCategoryId);
|
|
});
|
|
|
|
test('teaser edit page pre-fills partner product number', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, [
|
|
'partner_product_number' => 'MY-NUM-001',
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('partnerProductNumber', 'MY-NUM-001');
|
|
});
|
|
|
|
test('teaser edit saves updated partner product number', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('partnerProductNumber', 'UPDATED-123')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
expect($product->fresh()->partner_product_number)->toBe('UPDATED-123');
|
|
});
|
|
|
|
test('teaser edit maps pending status to active for UI', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, ['status' => ProductStatus::Pending]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('status', 'active');
|
|
});
|
|
|
|
test('teaser edit maps correction status to active for UI', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, [
|
|
'status' => ProductStatus::Correction,
|
|
'curation_notes' => 'Bitte Bild verbessern.',
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('status', 'active');
|
|
});
|
|
|
|
test('teaser edit maps draft status correctly', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, ['status' => ProductStatus::Draft]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('status', 'draft');
|
|
});
|
|
|
|
// --- Save / Update Tests ---
|
|
|
|
test('teaser edit saves updated product fields', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('name', 'Aktualisiertes Sideboard')
|
|
->set('descriptionShort', 'Neue Kurzbeschreibung.')
|
|
->set('priceDisplayText', 'Ab 4.000 €')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product->refresh();
|
|
|
|
expect($product->name)->toBe('Aktualisiertes Sideboard');
|
|
expect($product->description_short)->toBe('Neue Kurzbeschreibung.');
|
|
expect($product->price_display_text)->toBe('Ab 4.000 €');
|
|
});
|
|
|
|
test('teaser edit re-submits to pending when status is active', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, ['status' => ProductStatus::Draft]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('status', 'active')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product->refresh();
|
|
expect($product->status)->toBe(ProductStatus::Pending);
|
|
});
|
|
|
|
test('teaser edit keeps draft status when saving as draft', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, ['status' => ProductStatus::Pending]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('status', 'draft')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product->refresh();
|
|
expect($product->status)->toBe(ProductStatus::Draft);
|
|
});
|
|
|
|
test('teaser edit with correction status re-submits to pending', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, [
|
|
'status' => ProductStatus::Correction,
|
|
'curation_notes' => 'Bitte Bilder verbessern.',
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('status', 'active')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product->refresh();
|
|
expect($product->status)->toBe(ProductStatus::Pending);
|
|
});
|
|
|
|
test('teaser edit updates category', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$newCategory = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('categoryId', $newCategory->id)
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product->refresh();
|
|
expect($product->categories->first()->id)->toBe($newCategory->id);
|
|
});
|
|
|
|
test('teaser edit changes price type to on_request', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner, [
|
|
'price_type' => PriceType::FromPrice,
|
|
'price_display_text' => 'Ab 2.500 €',
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('priceType', 'on_request')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product->refresh();
|
|
expect($product->price_type)->toBe(PriceType::OnRequest);
|
|
});
|
|
|
|
// --- Activity Logging ---
|
|
|
|
test('teaser edit creates activity log entry on save', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('name', 'Updated Teaser Name')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$activity = ProductActivity::where('product_id', $product->id)->first();
|
|
|
|
expect($activity)->not->toBeNull();
|
|
expect($activity->action)->toBe('updated');
|
|
expect($activity->user_id)->toBe($user->id);
|
|
});
|
|
|
|
// --- Validation Tests ---
|
|
|
|
test('teaser edit requires name', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('name', '')
|
|
->call('save')
|
|
->assertHasErrors(['name' => 'required']);
|
|
});
|
|
|
|
test('teaser edit requires short description', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('descriptionShort', '')
|
|
->call('save')
|
|
->assertHasErrors(['descriptionShort' => 'required']);
|
|
});
|
|
|
|
test('teaser edit enforces max 180 chars for short description', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('descriptionShort', str_repeat('A', 181))
|
|
->call('save')
|
|
->assertHasErrors(['descriptionShort' => 'max']);
|
|
});
|
|
|
|
test('teaser edit requires category', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('categoryId', null)
|
|
->call('save')
|
|
->assertHasErrors(['categoryId' => 'required']);
|
|
});
|
|
|
|
test('teaser edit rejects fixed price type', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('priceType', 'fixed')
|
|
->call('save')
|
|
->assertHasErrors(['priceType']);
|
|
});
|
|
|
|
test('teaser edit requires price display text for from_price', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->set('priceType', 'from_price')
|
|
->set('priceDisplayText', '')
|
|
->call('save')
|
|
->assertHasErrors(['priceDisplayText']);
|
|
});
|
|
|
|
// --- Media Tests ---
|
|
|
|
test('teaser edit shows existing media', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$product->media()->create([
|
|
'file_path' => 'products/1/test.jpg',
|
|
'type' => 'image',
|
|
'alt_text' => 'Teaser Image',
|
|
'order_column' => 1,
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('existingMedia', fn ($value) => count($value) === 1 && $value[0]['alt_text'] === 'Teaser Image');
|
|
});
|
|
|
|
test('teaser edit can remove existing media', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
Storage::disk('public')->put('products/1/teaser.jpg', 'fake-image-content');
|
|
|
|
$media = $product->media()->create([
|
|
'file_path' => 'products/1/teaser.jpg',
|
|
'type' => 'image',
|
|
'alt_text' => 'Teaser Image',
|
|
'order_column' => 1,
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('existingMedia', fn ($value) => count($value) === 1)
|
|
->call('removeExistingMedia', $media->id)
|
|
->assertSet('existingMedia', fn ($value) => count($value) === 0);
|
|
|
|
expect($product->media()->count())->toBe(0);
|
|
Storage::disk('public')->assertMissing('products/1/teaser.jpg');
|
|
});
|
|
|
|
test('teaser edit can reorder existing media via drag and drop', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$media1 = $product->media()->create([
|
|
'file_path' => 'products/1/first.jpg',
|
|
'type' => 'image',
|
|
'alt_text' => 'First',
|
|
'order_column' => 1,
|
|
]);
|
|
$media2 = $product->media()->create([
|
|
'file_path' => 'products/1/second.jpg',
|
|
'type' => 'image',
|
|
'alt_text' => 'Second',
|
|
'order_column' => 2,
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('existingMedia', fn ($value) => count($value) === 2 && $value[0]['id'] === $media1->id)
|
|
->call('updateMediaOrder', [$media2->id, $media1->id])
|
|
->assertSet('existingMedia', fn ($value) => $value[0]['id'] === $media2->id && $value[1]['id'] === $media1->id);
|
|
|
|
expect($media2->fresh()->order_column)->toBe(1);
|
|
expect($media1->fresh()->order_column)->toBe(2);
|
|
});
|
|
|
|
test('teaser edit existing media is loaded sorted by order column', function () {
|
|
[$user, $partner] = createTeaserPartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$product = createTeaserProduct($partner);
|
|
|
|
$media2 = $product->media()->create([
|
|
'file_path' => 'products/1/second.jpg',
|
|
'type' => 'image',
|
|
'alt_text' => 'Second',
|
|
'order_column' => 2,
|
|
]);
|
|
$media1 = $product->media()->create([
|
|
'file_path' => 'products/1/first.jpg',
|
|
'type' => 'image',
|
|
'alt_text' => 'First',
|
|
'order_column' => 1,
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-teaser', ['product' => $product])
|
|
->assertSet('existingMedia', fn ($value) => $value[0]['id'] === $media1->id && $value[1]['id'] === $media2->id);
|
|
});
|