Add current_team_id to users so the last active team is restored on login instead of always defaulting to the personal team. When a user belongs to multiple teams and has no valid stored choice, redirect them to a new team.select screen (SelectTeam Livewire component) to pick one, rather than silently choosing the first team. Update Fortify and OAuth login flows to use the new resolveStoredTeam() logic.
139 lines
4.6 KiB
PHP
139 lines
4.6 KiB
PHP
<?php
|
|
|
|
use App\Livewire\SelectTeam;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
|
});
|
|
|
|
/**
|
|
* Create a user that belongs to two teams (their auto-created personal team
|
|
* plus a second team). Boarding is disabled so the middleware does not bounce
|
|
* the request to the onboarding screen.
|
|
*/
|
|
function userWithTwoTeams(): array
|
|
{
|
|
$user = User::factory()->create();
|
|
$personal = $user->teams->first();
|
|
$personal->update(['show_boarding' => false]);
|
|
|
|
$second = Team::factory()->create(['show_boarding' => false]);
|
|
$user->teams()->attach($second, ['role' => 'owner']);
|
|
$user->refresh();
|
|
|
|
return [$user, $personal, $second];
|
|
}
|
|
|
|
it('resolves the stored team when the user still belongs to it', function () {
|
|
[$user, , $second] = userWithTwoTeams();
|
|
$user->update(['current_team_id' => $second->id]);
|
|
|
|
expect($user->resolveStoredTeam()?->id)->toBe($second->id);
|
|
});
|
|
|
|
it('resolves the only team for single-team users without a stored choice', function () {
|
|
$user = User::factory()->create();
|
|
$user->teams->first()->update(['show_boarding' => false]);
|
|
|
|
expect($user->resolveStoredTeam()?->id)->toBe($user->teams->first()->id);
|
|
});
|
|
|
|
it('returns null for multi-team users without a valid stored choice', function () {
|
|
[$user] = userWithTwoTeams();
|
|
|
|
expect($user->resolveStoredTeam())->toBeNull();
|
|
});
|
|
|
|
it('ignores a stored team the user no longer belongs to', function () {
|
|
[$user, , $second] = userWithTwoTeams();
|
|
$user->update(['current_team_id' => 99999]);
|
|
|
|
expect($user->resolveStoredTeam())->toBeNull();
|
|
// still ambiguous (2 teams), so must pick again
|
|
$user->update(['current_team_id' => $second->id]);
|
|
$user->refresh();
|
|
expect($user->resolveStoredTeam()?->id)->toBe($second->id);
|
|
});
|
|
|
|
it('persists current_team_id when the active team changes via refreshSession', function () {
|
|
[$user, , $second] = userWithTwoTeams();
|
|
$this->actingAs($user);
|
|
|
|
refreshSession($second);
|
|
|
|
expect($user->fresh()->current_team_id)->toBe($second->id)
|
|
->and(data_get(session('currentTeam'), 'id'))->toBe($second->id);
|
|
});
|
|
|
|
it('redirects a multi-team user with no stored team to the select screen', function () {
|
|
[$user] = userWithTwoTeams();
|
|
|
|
$this->actingAs($user)
|
|
->get('/')
|
|
->assertRedirect(route('team.select'));
|
|
});
|
|
|
|
it('restores the stored team for a returning multi-team user', function () {
|
|
[$user, , $second] = userWithTwoTeams();
|
|
$user->update(['current_team_id' => $second->id]);
|
|
|
|
$this->actingAs($user)->get('/');
|
|
|
|
expect(data_get(session('currentTeam'), 'id'))->toBe($second->id);
|
|
});
|
|
|
|
it('does not send a single-team user to the select screen', function () {
|
|
$user = User::factory()->create();
|
|
$only = $user->teams->first();
|
|
$only->update(['show_boarding' => false]);
|
|
|
|
// A single-team user who lands on the select screen is bounced straight to
|
|
// the dashboard with their team activated, never shown a choice.
|
|
$this->actingAs($user)
|
|
->get(route('team.select'))
|
|
->assertRedirect(route('dashboard'));
|
|
|
|
expect(data_get(session('currentTeam'), 'id'))->toBe($only->id);
|
|
});
|
|
|
|
it('persists the choice and activates the team when selected on the screen', function () {
|
|
[$user, , $second] = userWithTwoTeams();
|
|
|
|
Livewire::actingAs($user)
|
|
->test(SelectTeam::class)
|
|
->call('selectTeam', $second->id)
|
|
->assertRedirect(route('dashboard'));
|
|
|
|
expect($user->fresh()->current_team_id)->toBe($second->id)
|
|
->and(data_get(session('currentTeam'), 'id'))->toBe($second->id);
|
|
});
|
|
|
|
it('lets the livewire update endpoint through for an ambiguous user', function () {
|
|
[$user] = userWithTwoTeams();
|
|
|
|
// The selection action runs as a Livewire AJAX POST to /livewire/update.
|
|
// The team gate must not hijack that request with a redirect to the
|
|
// selection screen, or the click silently does nothing (HTML != JSON).
|
|
$response = $this->actingAs($user)
|
|
->withHeaders(['X-Livewire' => 'true'])
|
|
->post('/livewire/update', []);
|
|
|
|
expect($response->headers->get('Location'))->not->toBe(route('team.select'));
|
|
});
|
|
|
|
it('bounces users who already have an active team away from the select screen', function () {
|
|
[$user, , $second] = userWithTwoTeams();
|
|
$user->update(['current_team_id' => $second->id]);
|
|
refreshSession($second);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(SelectTeam::class)
|
|
->assertRedirect(route('dashboard'));
|
|
});
|