create(); $role = Role::firstOrCreate(['name' => 'ROLE_ADMIN'], ['color' => '#111111']); $admin->roles()->attach($role); return $admin; } it('forbids non-admin role access', function (): void { $user = User::factory()->create(); Sanctum::actingAs($user); $response = $this->getJson('/api/roles'); $response->assertStatus(403); }); it('creates normalized roles as admin', function (): void { $admin = makeAdminForRoles(); Sanctum::actingAs($admin); $response = $this->postJson('/api/roles', [ 'name' => 'moderator', 'color' => '#abcdef', ]); $response->assertStatus(201); $response->assertJsonFragment([ 'name' => 'ROLE_MODERATOR', 'color' => '#abcdef', ]); $this->assertDatabaseHas('roles', [ 'name' => 'ROLE_MODERATOR', ]); }); it('lists roles for admins', function (): void { $admin = makeAdminForRoles(); Role::create(['name' => 'ROLE_ALPHA', 'color' => '#111111']); Sanctum::actingAs($admin); $response = $this->getJson('/api/roles'); $response->assertOk(); $response->assertJsonFragment(['name' => 'ROLE_ALPHA']); }); it('prevents renaming core roles', function (): void { $admin = makeAdminForRoles(); $core = Role::firstOrCreate(['name' => 'ROLE_ADMIN'], ['color' => '#111111']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/roles/{$core->id}", [ 'name' => 'ROLE_SUPER', 'color' => '#123456', ]); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Core roles cannot be renamed.']); }); it('prevents creating duplicate roles after normalization', function (): void { $admin = makeAdminForRoles(); Role::create(['name' => 'ROLE_TEST', 'color' => '#111111']); Sanctum::actingAs($admin); $response = $this->postJson('/api/roles', [ 'name' => 'test', 'color' => '#222222', ]); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Role already exists.']); }); it('updates role color when provided and keeps name', function (): void { $admin = makeAdminForRoles(); $role = Role::create(['name' => 'ROLE_EDIT', 'color' => '#111111']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/roles/{$role->id}", [ 'name' => 'ROLE_EDIT', 'color' => '#222222', ]); $response->assertOk(); $response->assertJsonFragment(['color' => '#222222']); }); it('prevents updating to duplicate normalized name', function (): void { $admin = makeAdminForRoles(); $first = Role::create(['name' => 'ROLE_FIRST', 'color' => '#111111']); $second = Role::create(['name' => 'ROLE_SECOND', 'color' => '#111111']); Sanctum::actingAs($admin); $response = $this->patchJson("/api/roles/{$second->id}", [ 'name' => 'first', 'color' => '#111111', ]); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Role already exists.']); }); it('prevents deleting core roles', function (): void { $admin = makeAdminForRoles(); $core = Role::firstOrCreate(['name' => 'ROLE_USER'], ['color' => '#111111']); Sanctum::actingAs($admin); $response = $this->deleteJson("/api/roles/{$core->id}"); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Core roles cannot be deleted.']); }); it('prevents deleting roles assigned to users', function (): void { $admin = makeAdminForRoles(); $role = Role::create(['name' => 'ROLE_HELPER', 'color' => '#222222']); $user = User::factory()->create(); $user->roles()->attach($role); Sanctum::actingAs($admin); $response = $this->deleteJson("/api/roles/{$role->id}"); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Role is assigned to users.']); }); it('deletes non-core roles without assignments', function (): void { $admin = makeAdminForRoles(); $role = Role::create(['name' => 'ROLE_CUSTOM', 'color' => '#333333']); Sanctum::actingAs($admin); $response = $this->deleteJson("/api/roles/{$role->id}"); $response->assertStatus(204); $this->assertDatabaseMissing('roles', ['id' => $role->id]); }); it('forbids non-admin create update delete', function (): void { $user = User::factory()->create(); Sanctum::actingAs($user); $response = $this->postJson('/api/roles', [ 'name' => 'helper', 'color' => '#111111', ]); $response->assertStatus(403); $role = Role::create(['name' => 'ROLE_TEMP', 'color' => '#111111']); $response = $this->patchJson("/api/roles/{$role->id}", [ 'name' => 'ROLE_TEMP', 'color' => '#222222', ]); $response->assertStatus(403); $response = $this->deleteJson("/api/roles/{$role->id}"); $response->assertStatus(403); }); it('normalizes invalid role names to ROLE_', function (): void { $admin = makeAdminForRoles(); Sanctum::actingAs($admin); $response = $this->postJson('/api/roles', [ 'name' => '!!!', 'color' => '#111111', ]); $response->assertStatus(201); $response->assertJsonFragment(['name' => 'ROLE_']); });