'array']); app(PermissionRegistrar::class)->forgetCachedPermissions(); // A small location hierarchy: country > division > two cities, plus another country $this->country = DB::table('countries')->insertGetId(['code' => 'XA', 'flag' => '', 'phonecode' => '9901']); $this->otherCountry = DB::table('countries')->insertGetId(['code' => 'XB', 'flag' => '', 'phonecode' => '9902']); $this->division = DB::table('divisions')->insertGetId(['country_id' => $this->country]); $this->city = DB::table('cities')->insertGetId(['country_id' => $this->country, 'division_id' => $this->division]); $this->otherCity = DB::table('cities')->insertGetId(['country_id' => $this->country, 'division_id' => $this->division]); } private function makePoll(array $attributes = []): Poll { $postId = DB::table('posts')->insertGetId([ 'postable_id' => 1, 'postable_type' => Admin::class, 'created_at' => now(), 'updated_at' => now(), ]); return Poll::create(array_merge(['post_id' => $postId, 'status' => Poll::STATUS_OPEN], $attributes)); } private function locate(Model $profile, int $country, ?int $division = null, ?int $city = null): Model { $profile->locations()->save(new Location([ 'country_id' => $country, 'division_id' => $division, 'city_id' => $city, ])); return $profile; } private function giveRole(User $user, Model $profile, string $role): void { $name = class_basename($profile) . '\\' . $profile->id . '\\' . $role; $user->assignRole(Role::firstOrCreate(['name' => $name, 'guard_name' => 'web'])); } public function test_untargeted_open_poll_is_shown_to_everyone(): void { $poll = $this->makePoll(); $user = User::factory()->create(); $this->assertTrue($poll->isTargetedAt($user, $user)); $this->assertTrue($poll->isTargetedAt(Organization::factory()->create(), $user)); $this->assertTrue($poll->isTargetedAt(Bank::factory()->create(), $user)); $this->assertTrue($poll->isTargetedAt(Admin::factory()->create(), $user)); } public function test_closed_draft_or_out_of_window_poll_is_hidden(): void { $user = User::factory()->create(); $this->assertFalse($this->makePoll(['status' => Poll::STATUS_DRAFT])->isTargetedAt($user, $user)); $this->assertFalse($this->makePoll(['status' => Poll::STATUS_CLOSED])->isTargetedAt($user, $user)); $this->assertFalse($this->makePoll(['opens_at' => now()->addHour()])->isTargetedAt($user, $user)); $this->assertFalse($this->makePoll(['closes_at' => now()->subHour()])->isTargetedAt($user, $user)); } public function test_open_now_scope_matches_is_open(): void { $open = $this->makePoll(['opens_at' => now()->subHour(), 'closes_at' => now()->addHour()]); $this->makePoll(['status' => Poll::STATUS_DRAFT]); $this->makePoll(['closes_at' => now()->subMinute()]); $this->makePoll(['opens_at' => now()->addMinute()]); $ids = Poll::openNow()->pluck('id'); $this->assertTrue($ids->contains($open->id)); $this->assertCount(1, $ids->intersect(Poll::latest('id')->take(4)->pluck('id'))); } public function test_profile_type_targeting(): void { $poll = $this->makePoll(['target_profile_types' => [Organization::class]]); $user = User::factory()->create(); $this->assertFalse($poll->isTargetedAt($user, $user)); $this->assertTrue($poll->isTargetedAt(Organization::factory()->create(), $user)); $this->assertFalse($poll->isTargetedAt(Bank::factory()->create(), $user)); } public function test_location_targeting_uses_deepest_selected_level(): void { $inCity = $this->locate(User::factory()->create(), $this->country, $this->division, $this->city); $inOtherCity = $this->locate(User::factory()->create(), $this->country, $this->division, $this->otherCity); $abroad = $this->locate(User::factory()->create(), $this->otherCountry); $nowhere = User::factory()->create(); $byCountry = $this->makePoll(['filter_by_location' => true, 'location_country_ids' => [$this->country]]); $this->assertTrue($byCountry->isTargetedAt($inCity, $inCity)); $this->assertTrue($byCountry->isTargetedAt($inOtherCity, $inOtherCity)); $this->assertFalse($byCountry->isTargetedAt($abroad, $abroad)); $this->assertFalse($byCountry->isTargetedAt($nowhere, $nowhere)); // Country and city selected: the city wins $byCity = $this->makePoll([ 'filter_by_location' => true, 'location_country_ids' => [$this->country], 'location_city_ids' => [$this->city], ]); $this->assertTrue($byCity->isTargetedAt($inCity, $inCity)); $this->assertFalse($byCity->isTargetedAt($inOtherCity, $inOtherCity)); // Location targeting switched off ignores the stored ids $off = $this->makePoll(['filter_by_location' => false, 'location_city_ids' => [$this->city]]); $this->assertTrue($off->isTargetedAt($abroad, $abroad)); } public function test_location_targeting_works_for_organizations(): void { $organization = $this->locate(Organization::factory()->create(), $this->country, $this->division, $this->city); $user = User::factory()->create(); $poll = $this->makePoll(['filter_by_location' => true, 'location_division_ids' => [$this->division]]); $this->assertTrue($poll->isTargetedAt($organization, $user)); $this->assertFalse($poll->isTargetedAt(Organization::factory()->create(), $user)); } public function test_last_login_range_is_inclusive_and_excludes_never_logged_in(): void { $poll = $this->makePoll(['last_login_from' => '2026-09-01', 'last_login_until' => '2026-09-30']); $first = User::factory()->create(['last_login_at' => '2026-09-01 00:05:00']); $last = User::factory()->create(['last_login_at' => '2026-09-30 23:55:00']); $before = User::factory()->create(['last_login_at' => '2026-08-31 23:59:00']); $never = User::factory()->create(['last_login_at' => null]); $this->assertTrue($poll->isTargetedAt($first, $first)); $this->assertTrue($poll->isTargetedAt($last, $last)); $this->assertFalse($poll->isTargetedAt($before, $before)); $this->assertFalse($poll->isTargetedAt($never, $never)); } public function test_registration_range(): void { $poll = $this->makePoll(['registered_until' => '2025-12-31']); $old = User::factory()->create(); $old->forceFill(['created_at' => '2025-06-01 12:00:00'])->saveQuietly(); $new = User::factory()->create(); $this->assertTrue($poll->isTargetedAt($old, $old)); $this->assertFalse($poll->isTargetedAt($new, $new)); $organization = Organization::factory()->create(); $this->assertFalse($poll->isTargetedAt($organization, $old)); } public function test_member_scope_managers_and_coordinators(): void { $poll = $this->makePoll(['member_scope' => Poll::MEMBER_SCOPE_MANAGERS_COORDINATORS]); $organization = Organization::factory()->create(); $bank = Bank::factory()->create(); [$manager, $coordinator, $plain] = User::factory()->count(3)->create(); $this->giveRole($manager, $organization, 'organization-manager'); $this->giveRole($coordinator, $organization, 'organization-coordinator'); $this->giveRole($manager, $bank, 'bank-manager'); $this->assertTrue($poll->isTargetedAt($organization, $manager)); $this->assertTrue($poll->isTargetedAt($organization, $coordinator)); $this->assertFalse($poll->isTargetedAt($organization, $plain)); $this->assertTrue($poll->isTargetedAt($bank, $manager)); $this->assertFalse($poll->isTargetedAt($bank, $coordinator)); // role on another profile // A role on a different organization does not count $otherOrganization = Organization::factory()->create(); $this->assertFalse($poll->isTargetedAt($otherOrganization, $manager)); // Admin profiles have no manager/coordinator roles: all linked users may vote $this->assertTrue($poll->isTargetedAt(Admin::factory()->create(), $plain)); // A User profile always votes for itself $this->assertTrue($poll->isTargetedAt($plain, $plain)); } public function test_member_scope_all_users(): void { $poll = $this->makePoll(['member_scope' => Poll::MEMBER_SCOPE_ALL_USERS]); $user = User::factory()->create(); $this->assertTrue($poll->isTargetedAt(Organization::factory()->create(), $user)); // Organization / Bank / Admin profiles always need the user behind them $this->assertFalse($poll->isTargetedAt(Organization::factory()->create(), null)); // A user cannot vote as another User profile $this->assertFalse($poll->isTargetedAt(User::factory()->create(), $user)); } public function test_response_lookup_and_vote_change(): void { $organization = Organization::factory()->create(); [$manager1, $manager2] = User::factory()->count(2)->create(); $perProfile = $this->makePoll(['vote_scope' => Poll::VOTE_SCOPE_PER_PROFILE]); $this->vote($perProfile, $organization, $manager1); // Any manager now sees the organization's vote $this->assertNotNull($perProfile->responseFor($organization, $manager2->id)); $this->assertFalse($perProfile->canSubmit($organization, $manager2->id)); $perUser = $this->makePoll(['vote_scope' => Poll::VOTE_SCOPE_PER_USER]); $this->vote($perUser, $organization, $manager1); $this->assertFalse($perUser->canSubmit($organization, $manager1->id)); $this->assertTrue($perUser->canSubmit($organization, $manager2->id)); $changeable = $this->makePoll(['allow_vote_change' => true]); $this->vote($changeable, $organization, $manager1); $this->assertTrue($changeable->canSubmit($organization, $manager1->id)); } private function vote(Poll $poll, Model $profile, User $user): PollResponse { return PollResponse::create([ 'poll_id' => $poll->id, 'voter_id' => $profile->id, 'voter_type' => get_class($profile), 'user_id' => $user->id, 'dedupe_key' => $poll->dedupeKey($profile, $user->id), 'submitted_at' => now(), ]); } }