'array', 'scout.driver' => null]); app(PermissionRegistrar::class)->forgetCachedPermissions(); app()->setLocale('en'); } /** A poll with one yes/no question; $from is the post's publication date (null = unpublished). */ private function makePoll(string $title, array $attributes = [], ?string $from = '2026-01-01 00:00:00'): Poll { $category = Category::firstOrCreate(['type' => Poll::class, 'categoryable_id' => null, 'categoryable_type' => null]); \App\Models\CategoryTranslation::firstOrCreate( ['category_id' => $category->id, 'locale' => 'en'], ['name' => 'Poll', 'slug' => 'poll-pages-test-' . $category->id] ); $postId = DB::table('posts')->insertGetId([ 'postable_id' => 1, 'postable_type' => Admin::class, 'category_id' => $category->id, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('post_translations')->insert([ 'post_id' => $postId, 'locale' => 'en', 'slug' => 'poll-pages-' . $postId, 'title' => $title, 'excerpt' => "Intro of $title", 'from' => $from, 'created_at' => now(), 'updated_at' => now(), ]); $poll = Poll::create(array_merge(['post_id' => $postId, 'status' => Poll::STATUS_OPEN], $attributes)); PollQuestion::create([ 'poll_id' => $poll->id, 'position' => 1, 'type' => 'yes_no', 'required' => true, 'label' => ['en' => "Question of $title"], 'settings' => [], ]); return $poll; } private function actAs(Model $profile, User $user): void { $this->actingAs($user, 'web'); session(['activeProfileType' => get_class($profile), 'activeProfileId' => $profile->getKey()]); } private function actAsAdmin(): void { $user = User::factory()->create(); $admin = Admin::factory()->create(); $admin->users()->attach($user); $role = Role::firstOrCreate(['name' => "Admin\\{$admin->id}\\poll-pages-role", 'guard_name' => 'web']); $role->givePermissionTo(Permission::firstOrCreate(['name' => 'manage polls', 'guard_name' => 'web'])); $user->assignRole($role); $this->actingAs($user, 'web'); $this->actingAs($admin, 'admin'); session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]); } private function vote(Poll $poll, User $user): void { PollResponse::create([ 'poll_id' => $poll->id, 'voter_id' => $user->id, 'voter_type' => User::class, 'user_id' => $user->id, 'dedupe_key' => $poll->dedupeKey($user, $user->id), 'submitted_at' => now(), ]); } // ----------------------------------------------------------------- // Publishing // ----------------------------------------------------------------- public function test_opening_a_poll_publishes_its_unpublished_post(): void { $this->actAsAdmin(); $poll = $this->makePoll('Unpublished poll', ['status' => Poll::STATUS_DRAFT], null); $translation = PostTranslation::where('post_id', $poll->post_id)->sole(); // Saving as draft leaves the post unpublished Livewire::test(Manage::class)->call('openEditModal', $poll->id)->call('savePoll'); $this->assertNull($translation->fresh()->from); Livewire::test(Manage::class) ->call('openEditModal', $poll->id) ->set('form.status', Poll::STATUS_OPEN) ->set('form.opens_at', '2026-11-01 09:00') ->call('savePoll') ->assertHasNoErrors(); $this->assertSame('2026-11-01 09:00', \Illuminate\Support\Carbon::parse($translation->fresh()->from)->format('Y-m-d H:i')); } public function test_an_existing_publication_date_is_kept_and_a_later_one_warned_about(): void { $this->actAsAdmin(); $poll = $this->makePoll('Late post', ['status' => Poll::STATUS_DRAFT], '2026-12-24 08:00:00'); $component = Livewire::test(Manage::class) ->call('openEditModal', $poll->id) ->set('tab', 'settings') ->assertDontSee('is published later than the poll opens') ->set('form.status', Poll::STATUS_OPEN) ->set('form.opens_at', '2026-11-01 09:00') ->assertSee('is published later than the poll opens') ->assertSee('EN:'); $this->assertArrayHasKey('en', $component->instance()->latePublications()); $component->call('savePoll')->assertHasNoErrors(); $this->assertSame('2026-12-24 08:00', \Illuminate\Support\Carbon::parse(PostTranslation::where('post_id', $poll->post_id)->sole()->from)->format('Y-m-d H:i')); // Opening on or after the publication date: no warning Livewire::test(Manage::class) ->call('openEditModal', $poll->id) ->set('form.opens_at', '2026-12-24 08:00') ->set('tab', 'settings') ->assertDontSee('is published later than the poll opens'); } // ----------------------------------------------------------------- // Main-page card // ----------------------------------------------------------------- public function test_card_shows_the_soonest_closing_unanswered_poll(): void { $user = User::factory()->create(); $this->actAs($user, $user); // Other open polls in the test database would compete for the card; close them for this test Poll::query()->update(['status' => Poll::STATUS_CLOSED]); $later = $this->makePoll('Closes later', ['closes_at' => now()->addDays(10)]); $sooner = $this->makePoll('Closes sooner', ['closes_at' => now()->addDays(2)]); $this->makePoll('Not for users', ['target_profile_types' => [Organization::class]]); $this->makePoll('Unpublished', [], null); Livewire::test(PollCardFull::class) ->assertSet('pollId', $sooner->id) ->assertSee('Closes sooner') ->assertSee('Intro of Closes sooner') ->assertSee('Question of Closes sooner'); // Answered: the next one takes its place $this->vote($sooner, $user); Livewire::test(PollCardFull::class)->assertSet('pollId', $later->id); $this->vote($later, $user); Livewire::test(PollCardFull::class)->assertSet('pollId', null)->assertDontSee('Question of'); } public function test_card_links_to_the_poll_page_for_several_questions(): void { $user = User::factory()->create(); $this->actAs($user, $user); Poll::query()->update(['status' => Poll::STATUS_CLOSED]); $poll = $this->makePoll('Long poll'); PollQuestion::create([ 'poll_id' => $poll->id, 'position' => 2, 'type' => 'yes_no', 'required' => true, 'label' => ['en' => 'Second question of Long poll'], 'settings' => [], ]); Livewire::test(PollCardFull::class) ->assertSet('pollId', $poll->id) ->assertSee('Long poll') ->assertSee('Intro of Long poll') ->assertSee('Take part') ->assertSee(route('poll.show', ['slug' => $this->slugOf($poll)]), false) ->assertDontSee('Question of Long poll') ->assertDontSee('Read more'); } public function test_card_shows_a_single_question_with_read_more_when_there_is_content(): void { $user = User::factory()->create(); $this->actAs($user, $user); Poll::query()->update(['status' => Poll::STATUS_CLOSED]); $poll = $this->makePoll('Short poll'); // No content: just the question Livewire::test(PollCardFull::class) ->assertSee('Question of Short poll') ->assertDontSee('Read more') ->assertDontSee('Take part'); // With content: "Read more..." to the poll page, above the question PostTranslation::where('post_id', $poll->post_id)->update(['content' => '
The background of this poll.
']); Livewire::test(PollCardFull::class) ->assertSeeInOrder(['Read more...', 'Question of Short poll']) ->assertSee(route('poll.show', ['slug' => $this->slugOf($poll)]), false) ->assertDontSee('The background of this poll.'); // Empty markup from the editor counts as no content PostTranslation::where('post_id', $poll->post_id)->update(['content' => 'Rich text
']); $html = $this->pollPage($this->slugOf($poll)); $this->assertStringContainsString('Dedicated page poll', $html); $this->assertStringContainsString('Intro of Dedicated page poll', $html); $this->assertStringContainsString('Rich text', $html); $this->assertStringNotContainsString('', $html); $this->assertStringContainsString('Question of Dedicated page poll', $html); $this->assertStringContainsString('Submit vote', $html); } public function test_unpublished_poll_page_is_only_for_post_admins(): void { $poll = $this->makePoll('Hidden poll', [], null); $slug = $this->slugOf($poll); $user = User::factory()->create(); $this->actAs($user, $user); $this->assertStringNotContainsString('Hidden poll', $this->pollPage($slug)); $this->actAsAdmin(); $this->assertStringContainsString('Hidden poll', $this->pollPage($slug)); } public function test_draft_poll_is_not_found_even_with_a_published_post(): void { // Published post, poll still a draft (security audit R3) $poll = $this->makePoll('Draft with published post', ['status' => Poll::STATUS_DRAFT]); $slug = $this->slugOf($poll); $user = User::factory()->create(); $this->actAs($user, $user); $this->assertStringNotContainsString('Draft with published post', $this->pollPage($slug)); $this->actAsAdmin(); $this->assertStringContainsString('Draft with published post', $this->pollPage($slug)); } public function test_only_admin_profiles_preview_unpublished_polls(): void { // An organization whose role somehow has poll rights still gets no preview (security audit R4) $poll = $this->makePoll('Unpublished for orgs', [], null); $user = User::factory()->create(); $organization = Organization::factory()->create(); $user->organizations()->attach($organization->id); $role = Role::firstOrCreate(['name' => "Organization\\{$organization->id}\\preview-role", 'guard_name' => 'web']); $role->givePermissionTo(Permission::firstOrCreate(['name' => 'manage polls', 'guard_name' => 'web'])); $user->assignRole($role); app(PermissionRegistrar::class)->forgetCachedPermissions(); $this->actAs($organization, $user); $this->assertTrue(\Illuminate\Support\Facades\Gate::allows('access polls')); // the permission is there... $this->assertStringNotContainsString('Unpublished for orgs', $this->pollPage($this->slugOf($poll))); // ...but no preview } public function test_post_pages_of_a_poll_redirect_to_the_poll_page(): void { $user = User::factory()->create(); $this->actAs($user, $user); $poll = $this->makePoll('Redirected poll'); $slug = $this->slugOf($poll); $posts = app(\App\Http\Controllers\PostController::class); $bySlug = $posts->showBySlug($slug); $this->assertInstanceOf(\Illuminate\Http\RedirectResponse::class, $bySlug); $this->assertSame(route('poll.show', ['slug' => $slug]), $bySlug->getTargetUrl()); $byId = $posts->showById($poll->post_id); $this->assertInstanceOf(\Illuminate\Http\RedirectResponse::class, $byId); $this->assertSame(route('poll.show', ['slug' => $slug]), $byId->getTargetUrl()); // Not a poll: not found on the poll route $this->assertStringNotContainsString('Redirected poll', $this->pollPage('no-such-slug')); } }