546 lines
20 KiB
PHP
546 lines
20 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Event;
|
|
use App\Models\EventPackage;
|
|
use App\Models\EventType;
|
|
use App\Models\Package;
|
|
use App\Models\PackagePurchase;
|
|
use App\Models\TenantPackage;
|
|
use App\Models\WatermarkSetting;
|
|
use App\Services\EventJoinTokenService;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\Feature\Tenant\TenantTestCase;
|
|
|
|
class EventControllerTest extends TenantTestCase
|
|
{
|
|
public function test_create_event_with_valid_package_succeeds(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$eventType = EventType::factory()->create();
|
|
$package = Package::factory()->create(['type' => 'endcustomer', 'max_photos' => 100]);
|
|
TenantPackage::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'package_id' => $package->id,
|
|
'active' => true,
|
|
]);
|
|
$purchase = PackagePurchase::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'package_id' => $package->id,
|
|
'type' => 'endcustomer_event',
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [
|
|
'name' => 'Test Event',
|
|
'slug' => 'test-event',
|
|
'event_date' => Carbon::now()->addDays(10)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'package_id' => $package->id,
|
|
'accepted_waiver' => true,
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$this->assertDatabaseHas('events', [
|
|
'tenant_id' => $tenant->id,
|
|
'name' => json_encode(['de' => 'Test Event']),
|
|
'slug' => 'test-event',
|
|
'event_type_id' => $eventType->id,
|
|
]);
|
|
|
|
$event = Event::latest()->first();
|
|
$this->assertDatabaseHas('event_packages', [
|
|
'event_id' => $event->id,
|
|
'package_id' => $package->id,
|
|
]);
|
|
|
|
$this->assertDatabaseHas('event_join_tokens', [
|
|
'event_id' => $event->id,
|
|
]);
|
|
|
|
$purchase->refresh();
|
|
$this->assertNotNull(data_get($purchase->metadata, 'consents.digital_content_waiver_at'));
|
|
}
|
|
|
|
public function test_create_event_without_package_fails(): void
|
|
{
|
|
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [
|
|
'name' => 'Test Event',
|
|
'slug' => 'test-event',
|
|
'event_date' => Carbon::now()->addDays(10)->toDateString(),
|
|
]);
|
|
|
|
$response->assertStatus(402)
|
|
->assertJsonPath('error.code', 'event_limit_missing');
|
|
}
|
|
|
|
public function test_superadmin_can_create_event_without_tenant_package(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$eventType = EventType::factory()->create();
|
|
$package = Package::factory()->create([
|
|
'type' => 'endcustomer',
|
|
'slug' => 'pro',
|
|
'max_photos' => 100,
|
|
]);
|
|
|
|
$superadmin = \App\Models\User::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'role' => 'superadmin',
|
|
'password' => Hash::make('password'),
|
|
'email_verified_at' => now(),
|
|
]);
|
|
|
|
$login = $this->postJson('/api/v1/tenant-auth/login', [
|
|
'login' => $superadmin->email,
|
|
'password' => 'password',
|
|
]);
|
|
|
|
$login->assertOk();
|
|
$token = (string) $login->json('token');
|
|
|
|
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
|
->postJson('/api/v1/tenant/events', [
|
|
'name' => 'Owner Event',
|
|
'slug' => 'owner-event',
|
|
'event_date' => Carbon::now()->addDays(10)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'package_id' => $package->id,
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$event = Event::latest()->first();
|
|
|
|
$this->assertDatabaseHas('events', [
|
|
'tenant_id' => $tenant->id,
|
|
'slug' => 'owner-event',
|
|
]);
|
|
|
|
$this->assertDatabaseHas('event_packages', [
|
|
'event_id' => $event->id,
|
|
'package_id' => $package->id,
|
|
]);
|
|
}
|
|
|
|
public function test_create_event_requires_waiver_for_endcustomer_package(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$eventType = EventType::factory()->create();
|
|
$package = Package::factory()->create(['type' => 'endcustomer', 'max_photos' => 100]);
|
|
TenantPackage::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'package_id' => $package->id,
|
|
'active' => true,
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [
|
|
'name' => 'Test Event',
|
|
'slug' => 'test-event',
|
|
'event_date' => Carbon::now()->addDays(10)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'package_id' => $package->id,
|
|
'accepted_waiver' => false,
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['accepted_waiver']);
|
|
}
|
|
|
|
public function test_create_event_with_reseller_package_limits_events(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$eventType = EventType::factory()->create();
|
|
$includedPackage = Package::factory()->endcustomer()->create([
|
|
'slug' => 'standard',
|
|
'gallery_days' => 30,
|
|
]);
|
|
$package = Package::factory()->create(['type' => 'reseller', 'max_events_per_year' => 1]);
|
|
TenantPackage::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'package_id' => $package->id,
|
|
'used_events' => 0,
|
|
'active' => true,
|
|
'expires_at' => null,
|
|
]);
|
|
|
|
// First event succeeds
|
|
$response1 = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [
|
|
'name' => 'First Event',
|
|
'slug' => 'first-event',
|
|
'event_date' => Carbon::now()->addDays(10)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'package_id' => $package->id, // Use reseller package for event? Adjust if needed
|
|
]);
|
|
|
|
$response1->assertStatus(201);
|
|
|
|
$event = Event::where('tenant_id', $tenant->id)->where('slug', 'first-event')->firstOrFail();
|
|
$this->assertDatabaseHas('event_packages', [
|
|
'event_id' => $event->id,
|
|
'package_id' => $includedPackage->id,
|
|
'purchased_price' => 0.00,
|
|
]);
|
|
|
|
// Second event fails due to limit
|
|
$response2 = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [
|
|
'name' => 'Second Event',
|
|
'slug' => 'second-event',
|
|
'event_date' => Carbon::now()->addDays(11)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'package_id' => $package->id,
|
|
]);
|
|
|
|
$response2->assertStatus(402)
|
|
->assertJsonPath('error.code', 'event_limit_exceeded');
|
|
}
|
|
|
|
public function test_update_event_settings_without_required_fields_succeeds(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'event_type_id' => $eventType->id,
|
|
'date' => Carbon::now()->subDays(2),
|
|
'name' => ['de' => 'Test Event', 'en' => 'Test Event'],
|
|
'settings' => [],
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
|
'settings' => [
|
|
'engagement_mode' => 'photo_only',
|
|
'guest_downloads_enabled' => false,
|
|
'guest_sharing_enabled' => true,
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
$event->refresh();
|
|
$this->assertSame('photo_only', data_get($event->settings, 'engagement_mode'));
|
|
$this->assertFalse((bool) data_get($event->settings, 'guest_downloads_enabled'));
|
|
$this->assertTrue((bool) data_get($event->settings, 'guest_sharing_enabled'));
|
|
}
|
|
|
|
public function test_create_event_rejects_unavailable_service_tier_for_partner_kontingent(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$eventType = EventType::factory()->create();
|
|
|
|
Package::factory()->endcustomer()->create(['slug' => 'standard', 'gallery_days' => 30]);
|
|
Package::factory()->endcustomer()->create(['slug' => 'pro', 'gallery_days' => 30]);
|
|
|
|
$partnerPackage = Package::factory()->reseller()->create([
|
|
'max_events_per_year' => 5,
|
|
'included_package_slug' => 'standard',
|
|
]);
|
|
|
|
TenantPackage::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'package_id' => $partnerPackage->id,
|
|
'used_events' => 0,
|
|
'active' => true,
|
|
'expires_at' => null,
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [
|
|
'name' => 'Premium Event',
|
|
'slug' => 'premium-event',
|
|
'event_date' => Carbon::now()->addDays(10)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'service_package_slug' => 'pro',
|
|
]);
|
|
|
|
$response->assertStatus(402)
|
|
->assertJsonPath('error.code', 'event_tier_unavailable');
|
|
}
|
|
|
|
public function test_update_event_accepts_live_show_settings(): void
|
|
{
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'Live Show Event',
|
|
'slug' => 'live-show-settings',
|
|
'date' => now()->addDays(5),
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
|
'name' => 'Live Show Event',
|
|
'event_date' => now()->addDays(5)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'settings' => [
|
|
'live_show' => [
|
|
'moderation_mode' => 'manual',
|
|
'retention_window_hours' => 12,
|
|
'playback_mode' => 'balanced',
|
|
'pace_mode' => 'fixed',
|
|
'fixed_interval_seconds' => 9,
|
|
'layout_mode' => 'single',
|
|
'effect_preset' => 'film_cut',
|
|
'effect_intensity' => 60,
|
|
'background_mode' => 'blur_last',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$event->refresh();
|
|
$settings = $event->settings;
|
|
$this->assertSame(['de' => 'Live Show Event'], $event->name);
|
|
$this->assertSame('manual', data_get($settings, 'live_show.moderation_mode'));
|
|
$this->assertSame(12, data_get($settings, 'live_show.retention_window_hours'));
|
|
$this->assertSame('balanced', data_get($settings, 'live_show.playback_mode'));
|
|
$this->assertSame('fixed', data_get($settings, 'live_show.pace_mode'));
|
|
$this->assertSame(9, data_get($settings, 'live_show.fixed_interval_seconds'));
|
|
$this->assertSame('single', data_get($settings, 'live_show.layout_mode'));
|
|
$this->assertSame('film_cut', data_get($settings, 'live_show.effect_preset'));
|
|
$this->assertSame(60, data_get($settings, 'live_show.effect_intensity'));
|
|
$this->assertSame('blur_last', data_get($settings, 'live_show.background_mode'));
|
|
}
|
|
|
|
public function test_update_event_accepts_svg_watermark_data_url(): void
|
|
{
|
|
Storage::fake('public');
|
|
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'SVG Watermark Event',
|
|
'slug' => 'svg-watermark',
|
|
'date' => now()->addDays(2),
|
|
]);
|
|
|
|
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20"></svg>';
|
|
$dataUrl = 'data:image/svg+xml;base64,'.base64_encode($svg);
|
|
|
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
|
'settings' => [
|
|
'watermark' => [
|
|
'mode' => 'custom',
|
|
'asset_data_url' => $dataUrl,
|
|
'position' => 'bottom-right',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$event->refresh();
|
|
$path = data_get($event->settings, 'watermark.asset');
|
|
$this->assertSame("branding/watermarks/event-{$event->id}.svg", $path);
|
|
Storage::disk('public')->assertExists($path);
|
|
}
|
|
|
|
public function test_show_event_includes_signed_watermark_asset_url(): void
|
|
{
|
|
Storage::fake('public');
|
|
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'Watermark Preview Event',
|
|
'slug' => 'watermark-preview',
|
|
'date' => now()->addDays(2),
|
|
'settings' => [
|
|
'watermark' => [
|
|
'mode' => 'custom',
|
|
'asset' => 'branding/watermarks/event-123.png',
|
|
],
|
|
],
|
|
]);
|
|
|
|
Storage::disk('public')->put('branding/watermarks/event-123.png', 'asset');
|
|
|
|
$response = $this->authenticatedRequest('GET', "/api/v1/tenant/events/{$event->slug}");
|
|
|
|
$response->assertOk();
|
|
$url = (string) $response->json('data.settings.watermark.asset_url');
|
|
$this->assertNotSame('', $url);
|
|
$this->assertStringContainsString('/api/v1/branding/asset/branding/watermarks/event-123.png', $url);
|
|
$this->assertStringContainsString('signature=', $url);
|
|
}
|
|
|
|
public function test_show_event_includes_base_watermark_asset_url_when_missing_settings(): void
|
|
{
|
|
Storage::fake('public');
|
|
|
|
$setting = WatermarkSetting::query()->create([
|
|
'asset' => 'branding/watermarks/base-watermark.png',
|
|
'position' => 'bottom-right',
|
|
'opacity' => 0.25,
|
|
'scale' => 0.2,
|
|
'padding' => 16,
|
|
]);
|
|
|
|
Storage::disk('public')->put('branding/watermarks/base-watermark.png', 'asset');
|
|
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'Base Watermark Preview',
|
|
'slug' => 'base-watermark-preview',
|
|
'date' => now()->addDays(2),
|
|
'settings' => [],
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('GET', "/api/v1/tenant/events/{$event->slug}");
|
|
|
|
$response->assertOk();
|
|
$this->assertSame('base', $response->json('data.settings.watermark.mode'));
|
|
$url = (string) $response->json('data.settings.watermark.asset_url');
|
|
$this->assertNotSame('', $url);
|
|
$this->assertStringContainsString('/api/v1/branding/asset/branding/watermarks/base-watermark.png', $url);
|
|
$this->assertStringContainsString('signature=', $url);
|
|
$this->assertSame($setting->asset, $response->json('data.settings.watermark.asset'));
|
|
}
|
|
|
|
public function test_update_event_allows_disabling_watermark_when_removal_is_enabled(): void
|
|
{
|
|
$package = Package::factory()->create([
|
|
'watermark_allowed' => true,
|
|
'branding_allowed' => true,
|
|
'features' => ['no_watermark'],
|
|
]);
|
|
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'Removal Allowed Event',
|
|
'slug' => 'removal-allowed',
|
|
'date' => now()->addDays(2),
|
|
]);
|
|
|
|
EventPackage::create([
|
|
'event_id' => $event->id,
|
|
'package_id' => $package->id,
|
|
'purchased_price' => $package->price ?? 0,
|
|
'used_photos' => 0,
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
|
'settings' => [
|
|
'watermark' => [
|
|
'mode' => 'off',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$event->refresh();
|
|
$this->assertSame('off', data_get($event->settings, 'watermark.mode'));
|
|
$this->assertTrue((bool) data_get($event->settings, 'watermark_removal_allowed'));
|
|
}
|
|
|
|
public function test_update_event_forces_base_watermark_when_removal_is_disabled(): void
|
|
{
|
|
$package = Package::factory()->create([
|
|
'watermark_allowed' => true,
|
|
'branding_allowed' => true,
|
|
'features' => [],
|
|
]);
|
|
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'Removal Disabled Event',
|
|
'slug' => 'removal-disabled',
|
|
'date' => now()->addDays(2),
|
|
]);
|
|
|
|
EventPackage::create([
|
|
'event_id' => $event->id,
|
|
'package_id' => $package->id,
|
|
'purchased_price' => $package->price ?? 0,
|
|
'used_photos' => 0,
|
|
]);
|
|
|
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
|
'settings' => [
|
|
'watermark' => [
|
|
'mode' => 'off',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$event->refresh();
|
|
$this->assertSame('base', data_get($event->settings, 'watermark.mode'));
|
|
$this->assertFalse((bool) data_get($event->settings, 'watermark_removal_allowed'));
|
|
}
|
|
|
|
public function test_update_event_uploads_branding_logo_data_url(): void
|
|
{
|
|
Storage::fake('public');
|
|
|
|
$eventType = EventType::factory()->create();
|
|
$event = Event::factory()->for($this->tenant)->create([
|
|
'event_type_id' => $eventType->id,
|
|
'name' => 'Branding Event',
|
|
'slug' => 'branding-event',
|
|
'date' => now()->addDays(5),
|
|
]);
|
|
|
|
$logoFile = UploadedFile::fake()->image('logo.png', 64, 64);
|
|
$logoContents = file_get_contents($logoFile->getRealPath());
|
|
$this->assertIsString($logoContents);
|
|
$logoDataUrl = 'data:image/png;base64,'.base64_encode($logoContents);
|
|
|
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
|
'name' => 'Branding Event',
|
|
'event_date' => now()->addDays(5)->toDateString(),
|
|
'event_type_id' => $eventType->id,
|
|
'settings' => [
|
|
'branding' => [
|
|
'logo_data_url' => $logoDataUrl,
|
|
'logo' => [
|
|
'mode' => 'upload',
|
|
'value' => $logoDataUrl,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$event->refresh();
|
|
$logoPath = (string) data_get($event->settings, 'branding.logo_url');
|
|
$this->assertNotEmpty($logoPath);
|
|
Storage::disk('public')->assertExists($logoPath);
|
|
$this->assertSame($logoPath, data_get($event->settings, 'branding.logo.value'));
|
|
$this->assertNull(data_get($event->settings, 'branding.logo_data_url'));
|
|
}
|
|
|
|
public function test_upload_exceeds_package_limit_fails(): void
|
|
{
|
|
$tenant = $this->tenant;
|
|
$event = Event::factory()->create(['tenant_id' => $tenant->id, 'status' => 'published']);
|
|
$package = Package::factory()->create(['type' => 'endcustomer', 'max_photos' => 0]); // Limit 0
|
|
EventPackage::create([
|
|
'event_id' => $event->id,
|
|
'package_id' => $package->id,
|
|
'purchased_price' => $package->price,
|
|
'used_photos' => 0,
|
|
]);
|
|
|
|
Storage::fake('public');
|
|
$token = app(EventJoinTokenService::class)->createToken($event);
|
|
|
|
$response = $this->withHeader('X-Device-Id', 'limit-test')
|
|
->post("/api/v1/events/{$token->token}/upload", [
|
|
'photo' => UploadedFile::fake()->image('limit.jpg'),
|
|
]);
|
|
|
|
$response->assertStatus(402)
|
|
->assertJsonPath('error.code', 'photo_limit_exceeded');
|
|
}
|
|
}
|