Files
SkinbaseNova/tests/Feature/Worlds/WorldRecurrenceWorkflowTest.php
2026-04-25 08:36:03 +02:00

176 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\User;
use App\Models\World;
use App\Models\WorldRelation;
use App\Services\Worlds\WorldService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
function recurringWorldModerator(): User
{
return User::factory()->create([
'role' => 'moderator',
'username' => 'recurrence-mod-' . Str::lower(Str::random(6)),
'name' => 'Recurrence Moderator',
]);
}
function studioRecurringWorld(User $creator, array $attributes = []): World
{
return World::factory()->create(array_merge([
'created_by_user_id' => $creator->id,
'title' => 'Retro Month 2026',
'slug' => 'retro-month-2026',
'status' => World::STATUS_PUBLISHED,
'type' => World::TYPE_EVENT,
'starts_at' => Carbon::now()->subDays(14),
'ends_at' => Carbon::now()->addDays(7),
'promotion_starts_at' => Carbon::now()->subDays(10),
'promotion_ends_at' => Carbon::now()->addDays(5),
'submission_starts_at' => Carbon::now()->subDays(7),
'submission_ends_at' => Carbon::now()->addDays(5),
'published_at' => Carbon::now()->subDays(21),
'is_active_campaign' => true,
'is_homepage_featured' => true,
'campaign_priority' => 200,
'is_recurring' => true,
'recurrence_key' => 'retro-month',
'recurrence_rule' => 'yearly',
'edition_year' => 2026,
'cta_url' => 'https://skinbase.test/worlds/retro-month',
'badge_url' => 'https://skinbase.test/badges/retro-month',
], $attributes));
}
it('creates a clean next edition draft for recurring worlds', function (): void {
$moderator = recurringWorldModerator();
$source = studioRecurringWorld($moderator);
WorldRelation::query()->create([
'world_id' => $source->id,
'section_key' => 'featured_artworks',
'related_type' => WorldRelation::TYPE_ARTWORK,
'related_id' => 123,
'context_label' => 'Carry-over candidate',
'sort_order' => 0,
'is_featured' => true,
]);
$this->actingAs($moderator)
->from(route('studio.worlds.edit', ['world' => $source->id]))
->post(route('studio.worlds.new-edition', ['world' => $source->id]), [
'copy_mode' => WorldService::COPY_MODE_STRUCTURE_ONLY,
]);
$edition = World::query()
->whereKeyNot($source->id)
->latest('id')
->firstOrFail();
expect($edition->title)->toBe('Retro Month 2027')
->and($edition->slug)->toBe('retro-month-2027')
->and($edition->status)->toBe(World::STATUS_DRAFT)
->and($edition->is_recurring)->toBeTrue()
->and($edition->recurrence_key)->toBe('retro-month')
->and($edition->recurrence_rule)->toBe('yearly')
->and($edition->edition_year)->toBe(2027)
->and($edition->parent_world_id)->toBe($source->id)
->and($edition->starts_at)->toBeNull()
->and($edition->ends_at)->toBeNull()
->and($edition->promotion_starts_at)->toBeNull()
->and($edition->promotion_ends_at)->toBeNull()
->and($edition->submission_starts_at)->toBeNull()
->and($edition->submission_ends_at)->toBeNull()
->and($edition->published_at)->toBeNull()
->and($edition->is_active_campaign)->toBeFalse()
->and($edition->is_homepage_featured)->toBeFalse()
->and($edition->campaign_priority)->toBeNull()
->and($edition->cta_url)->toBeNull()
->and($edition->badge_url)->toBeNull()
->and($edition->worldRelations()->count())->toBe(0);
});
it('rejects next edition creation for non-recurring worlds', function (): void {
$moderator = recurringWorldModerator();
$world = World::factory()->create([
'created_by_user_id' => $moderator->id,
'title' => 'One-Off Showcase 2026',
'slug' => 'one-off-showcase-2026',
'status' => World::STATUS_PUBLISHED,
'type' => World::TYPE_EVENT,
'is_recurring' => false,
'recurrence_key' => null,
'recurrence_rule' => null,
'edition_year' => null,
]);
$this->actingAs($moderator)
->from(route('studio.worlds.edit', ['world' => $world->id]))
->post(route('studio.worlds.new-edition', ['world' => $world->id]))
->assertSessionHasErrors(['recurrence_key']);
expect(World::query()->count())->toBe(1);
});
it('rejects duplicate recurrence years when storing worlds', function (): void {
$moderator = recurringWorldModerator();
studioRecurringWorld($moderator, [
'title' => 'Pixel Week 2026',
'slug' => 'pixel-week-2026',
'recurrence_key' => 'pixel-week',
'edition_year' => 2026,
]);
$this->actingAs($moderator)
->from(route('studio.worlds.create'))
->post(route('studio.worlds.store'), [
'title' => 'Pixel Week Draft',
'slug' => 'pixel-week-draft',
'status' => World::STATUS_DRAFT,
'type' => World::TYPE_EVENT,
'is_recurring' => true,
'recurrence_key' => 'pixel-week',
'edition_year' => 2026,
])
->assertSessionHasErrors(['edition_year']);
expect(World::query()->count())->toBe(1);
});
it('rejects publishing a second current edition for the same recurrence family', function (): void {
$moderator = recurringWorldModerator();
studioRecurringWorld($moderator, [
'title' => 'Spring Vibes 2026',
'slug' => 'spring-vibes-2026',
'recurrence_key' => 'spring-vibes',
'edition_year' => 2026,
'status' => World::STATUS_PUBLISHED,
'starts_at' => Carbon::now()->subDays(3),
'ends_at' => Carbon::now()->addDays(10),
]);
$this->actingAs($moderator)
->from(route('studio.worlds.create'))
->post(route('studio.worlds.store'), [
'title' => 'Spring Vibes 2027',
'slug' => 'spring-vibes-2027',
'status' => World::STATUS_PUBLISHED,
'type' => World::TYPE_EVENT,
'starts_at' => Carbon::now()->subDay()->toIso8601String(),
'ends_at' => Carbon::now()->addDays(14)->toIso8601String(),
'is_recurring' => true,
'recurrence_key' => 'spring-vibes',
'edition_year' => 2027,
])
->assertSessionHasErrors(['status']);
expect(World::query()->count())->toBe(1);
});