create(); $role = Role::firstOrCreate(['name' => 'ROLE_ADMIN'], ['color' => '#111111']); $admin->roles()->attach($role); return $admin; } it('requires authentication to list users', function (): void { $response = $this->getJson('/api/users'); $response->assertStatus(401); }); it('lists users with roles and group color', function (): void { $admin = makeAdmin(); $role = Role::firstOrCreate(['name' => 'ROLE_MOD'], ['color' => '#ff0000']); $user = User::factory()->create(['name' => 'Alice']); $user->roles()->attach($role); Sanctum::actingAs($admin); $response = $this->getJson('/api/users'); $response->assertOk(); $response->assertJsonFragment([ 'id' => $user->id, 'name' => 'Alice', 'group_color' => '#ff0000', ]); }); it('returns current user profile from me endpoint', function (): void { $user = User::factory()->create(['name' => 'Me']); Sanctum::actingAs($user); $response = $this->getJson('/api/user/me'); $response->assertOk(); $response->assertJsonFragment([ 'id' => $user->id, 'name' => 'Me', 'email' => $user->email, ]); }); it('rejects unauthenticated me requests', function (): void { $response = $this->getJson('/api/user/me'); $response->assertStatus(401); $response->assertJsonFragment(['message' => 'Unauthenticated.']); }); it('returns user profile details', function (): void { $viewer = User::factory()->create(); $target = User::factory()->create(['name' => 'ProfileUser']); Sanctum::actingAs($viewer); $response = $this->getJson("/api/user/profile/{$target->id}"); $response->assertOk(); $response->assertJsonFragment([ 'id' => $target->id, 'name' => 'ProfileUser', ]); }); it('updates user location via updateMe', function (): void { $user = User::factory()->create(['location' => null]); Sanctum::actingAs($user); $response = $this->patchJson('/api/user/me', [ 'location' => ' New York ', ]); $response->assertOk(); $response->assertJsonFragment([ 'id' => $user->id, 'location' => 'New York', ]); $user->refresh(); expect($user->location)->toBe('New York'); }); it('rejects updateMe when unauthenticated', function (): void { $response = $this->patchJson('/api/user/me', [ 'location' => 'Somewhere', ]); $response->assertStatus(401); $response->assertJsonFragment(['message' => 'Unauthenticated.']); }); it('clears location when updateMe receives blank value', function (): void { $user = User::factory()->create(['location' => 'Somewhere']); Sanctum::actingAs($user); $response = $this->patchJson('/api/user/me', [ 'location' => ' ', ]); $response->assertOk(); $response->assertJsonFragment([ 'id' => $user->id, 'location' => null, ]); $user->refresh(); expect($user->location)->toBeNull(); }); it('forbids non-admin rank updates', function (): void { $user = User::factory()->create(); $target = User::factory()->create(); $rank = Rank::create(['name' => 'Silver']); Sanctum::actingAs($user); $response = $this->patchJson("/api/users/{$target->id}/rank", [ 'rank_id' => $rank->id, ]); $response->assertStatus(403); }); it('forbids founder rank updates by non-founder admin', function (): void { $admin = makeAdmin(); $founderRole = Role::firstOrCreate(['name' => 'ROLE_FOUNDER'], ['color' => '#111111']); $founder = User::factory()->create(); $founder->roles()->attach($founderRole); $rank = Rank::create(['name' => 'Founder Rank']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$founder->id}/rank", [ 'rank_id' => $rank->id, ]); $response->assertStatus(403); }); it('allows admins to update user rank', function (): void { $admin = makeAdmin(); $target = User::factory()->create(); $rank = Rank::create(['name' => 'Gold']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$target->id}/rank", [ 'rank_id' => $rank->id, ]); $response->assertOk(); $response->assertJsonPath('id', $target->id); $response->assertJsonPath('rank.id', $rank->id); $response->assertJsonPath('rank.name', 'Gold'); $target->refresh(); expect($target->rank_id)->toBe($rank->id); }); it('rejects update without admin role', function (): void { $user = User::factory()->create(); $target = User::factory()->create(); Sanctum::actingAs($user); $response = $this->patchJson("/api/users/{$target->id}", [ 'name' => 'New Name', 'email' => 'new@example.com', 'rank_id' => null, ]); $response->assertStatus(403); }); it('forbids updating founder user when actor is not founder', function (): void { $admin = makeAdmin(); $founderRole = Role::firstOrCreate(['name' => 'ROLE_FOUNDER'], ['color' => '#111111']); $founder = User::factory()->create(); $founder->roles()->attach($founderRole); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$founder->id}", [ 'name' => 'New Name', 'email' => 'new@example.com', 'rank_id' => null, ]); $response->assertStatus(403); }); it('rejects assigning founder role for non-founder admin', function (): void { $admin = makeAdmin(); $target = User::factory()->create(); Role::firstOrCreate(['name' => 'ROLE_FOUNDER'], ['color' => '#111111']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$target->id}", [ 'name' => 'New Name', 'email' => 'new@example.com', 'rank_id' => null, 'roles' => ['ROLE_FOUNDER'], ]); $response->assertStatus(403); $response->assertJsonFragment(['message' => 'Forbidden']); }); it('rejects duplicate canonical names', function (): void { $admin = makeAdmin(); User::factory()->create([ 'name' => 'Dupe', 'name_canonical' => 'dupe', 'email' => 'dupe@example.com', ]); $target = User::factory()->create([ 'name' => 'Other', 'name_canonical' => 'other', 'email' => 'other@example.com', ]); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$target->id}", [ 'name' => 'Dupe', 'email' => 'other@example.com', 'rank_id' => null, ]); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Name already exists.']); }); it('normalizes roles and updates group color', function (): void { $admin = makeAdmin(); $target = User::factory()->create([ 'name' => 'Target', 'email' => 'target@example.com', ]); Role::firstOrCreate(['name' => 'ROLE_MOD'], ['color' => '#00ff00']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$target->id}", [ 'name' => 'Target', 'email' => 'target@example.com', 'rank_id' => null, 'roles' => ['ROLE_MOD'], ]); $response->assertOk(); $response->assertJsonFragment(['group_color' => '#00ff00']); }); it('updates user name and email as admin', function (): void { $admin = makeAdmin(); $target = User::factory()->create([ 'name' => 'Old Name', 'email' => 'old@example.com', 'email_verified_at' => now(), ]); Role::firstOrCreate(['name' => 'ROLE_MOD'], ['color' => '#00aa00']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/users/{$target->id}", [ 'name' => 'New Name', 'email' => 'new@example.com', 'rank_id' => null, 'roles' => ['ROLE_MOD'], ]); $response->assertOk(); $response->assertJsonFragment([ 'id' => $target->id, 'name' => 'New Name', 'email' => 'new@example.com', ]); $target->refresh(); expect($target->name)->toBe('New Name'); expect($target->email)->toBe('new@example.com'); expect($target->email_verified_at)->toBeNull(); });