Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Polls;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Bank;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollResponse;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Who a poll is shown to: open window, profile type, location, last login,
|
||||
* registration date, member scope, and whether the voter already voted.
|
||||
*/
|
||||
class PollTargetingTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
private int $country;
|
||||
private int $otherCountry;
|
||||
private int $division;
|
||||
private int $city;
|
||||
private int $otherCity;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// phpunit.xml configures Redis, which is the dev site's cache too
|
||||
config(['cache.default' => '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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user