UDT-004: Gestión de Roles (tabla maestra + CRUD admin + validator dinámico + UI) #8

Merged
dmolinari merged 4 commits from feature/UDT-004 into main 2026-04-15 16:19:58 +00:00
Owner

Summary

  • Tabla maestra dbo.Rol con 8 roles canónicos del RBAC doc (§2.4.2) + FK Usuario.Rol → Rol.Codigo reemplazando el CHECK constraint hardcodeado.
  • CRUD admin-only /api/v1/roles (list / get / create / update / soft-delete con guard 409 por usuarios activos).
  • Refactor CreateUsuarioCommandValidator de whitelist hardcoded a lookup async vía IRolRepository.ExistsActiveByCodigoAsync.
  • Frontend: feature completa /admin/roles (list + create + edit + deactivate) y UserForm migrado a dropdown dinámico con loading/error states.

Capabilities

  • NEW role-management — 6 requirements, 13 scenarios (todos compliant).
  • MODIFIED user-registration — Request Validation ahora consulta la tabla Rol en tiempo real (5 scenarios cubiertos).

Tests

Suite Resultado
SIGCM2.Application.Tests 173/173
SIGCM2.Api.Tests 29/29
Frontend vitest (8 files) 47/47
Total 249
dotnet build 0 errors, 0 warnings
tsc -b clean

Migraciones aplicadas

  • V003__create_rol.sql — crea dbo.Rol + CK formato codigo + MERGE idempotente con 8 seeds (admin, cajero, operador_ctacte, picadora, jefe_publicidad, productor, diagramacion, reportes).
  • V004__alter_usuario_rol_fk.sql — DROP CK_Usuario_Rol + ADD FK_Usuario_Rol.
  • Aplicadas en SIGCM2 y SIGCM2_Test.
  • Data migration: UPDATE Usuario SET Rol='cajero' WHERE Username='dmolinari' (mapeo canónico del legacy placeholder vendedor).

Decisiones arquitecturales

  1. Natural key Codigo como FK vs surrogate RolId INT — mantiene JWT claim string, [Authorize(Roles=...)] nativo, contrato HTTP legible.
  2. Validator async sin cache — query trivial SELECT 1 FROM Rol WHERE Codigo=@c AND Activo=1.
  3. Soft-delete con guard 409HasActiveUsuariosAsync bloquea desactivación si hay usuarios referenciando.
  4. [Authorize(Roles="admin")] nativo — paridad con UsuariosController; RequirePermission granular llega en UDT-006.

Infra testing (deviations documentadas)

  • IClassFixture<TestWebAppFactory>ICollectionFixture<TestWebAppFactory> (shared factory) — resuelve ObjectDisposedException: RSABCrypt cross-class.
  • tests/tests.runsettings con MaxCpuCount=1 — evita race entre assemblies sobre SIGCM2_Test.
  • SeedRolCanonicalAsync post-Respawn en 4 fixtures — Respawn TablesToIgnore no respetado en 6.2.1.

Test plan

  • Login como admin.
  • Navegar a /admin/roles — ver los 8 seeds con badges Activo/Inactivo.
  • Crear rol nuevo (ej. cajero_senior) — validar regex codigo ^[a-z][a-z0-9_]*$.
  • Editar un rol — modificar nombre/descripción, togglear Activo.
  • Desactivar un rol sin usuarios → 204.
  • Intentar desactivar admin → 409 (tiene el usuario admin asociado).
  • Ir a /usuarios/nuevo — confirmar que el dropdown de Rol se puebla async con los activos y NO incluye inactivos.
  • Crear usuario con un rol canónico → 201.
  • Crear usuario con rol inactivo (curl/Postman) → 400 con errors.Rol.

Out of Scope (diferidos)

  • Permisos granulares y RolPermisosUDT-005.
  • RequirePermission middleware → UDT-006.
  • Herencia de roles (heredaDe) → UDT-005.
  • Auditoría de cambios de roles → UDT futura.

Artifacts SDD (engram)

Audit trail completo en engram (topic_keys):

  • sdd/udt-004-gestion-roles/{explore,proposal,spec,design,tasks,apply-progress,verify-report,archive-report}
  • Observation IDs: #295#301 + archive report.
## Summary - Tabla maestra `dbo.Rol` con 8 roles canónicos del RBAC doc (§2.4.2) + FK `Usuario.Rol → Rol.Codigo` reemplazando el CHECK constraint hardcodeado. - CRUD admin-only `/api/v1/roles` (list / get / create / update / soft-delete con guard 409 por usuarios activos). - Refactor `CreateUsuarioCommandValidator` de whitelist hardcoded a lookup async vía `IRolRepository.ExistsActiveByCodigoAsync`. - Frontend: feature completa `/admin/roles` (list + create + edit + deactivate) y `UserForm` migrado a dropdown dinámico con loading/error states. ## Capabilities - **NEW** `role-management` — 6 requirements, 13 scenarios (todos compliant). - **MODIFIED** `user-registration` — Request Validation ahora consulta la tabla Rol en tiempo real (5 scenarios cubiertos). ## Tests | Suite | Resultado | |-------|-----------| | `SIGCM2.Application.Tests` | **173/173** ✅ | | `SIGCM2.Api.Tests` | **29/29** ✅ | | Frontend `vitest` (8 files) | **47/47** ✅ | | **Total** | **249** ✅ | | `dotnet build` | 0 errors, 0 warnings | | `tsc -b` | clean | ## Migraciones aplicadas - `V003__create_rol.sql` — crea `dbo.Rol` + CK formato codigo + MERGE idempotente con 8 seeds (`admin`, `cajero`, `operador_ctacte`, `picadora`, `jefe_publicidad`, `productor`, `diagramacion`, `reportes`). - `V004__alter_usuario_rol_fk.sql` — DROP `CK_Usuario_Rol` + ADD `FK_Usuario_Rol`. - Aplicadas en `SIGCM2` y `SIGCM2_Test`. - **Data migration**: `UPDATE Usuario SET Rol='cajero' WHERE Username='dmolinari'` (mapeo canónico del legacy placeholder `vendedor`). ## Decisiones arquitecturales 1. **Natural key `Codigo`** como FK vs surrogate `RolId INT` — mantiene JWT claim string, `[Authorize(Roles=...)]` nativo, contrato HTTP legible. 2. **Validator async sin cache** — query trivial `SELECT 1 FROM Rol WHERE Codigo=@c AND Activo=1`. 3. **Soft-delete con guard 409** — `HasActiveUsuariosAsync` bloquea desactivación si hay usuarios referenciando. 4. **`[Authorize(Roles="admin")]` nativo** — paridad con `UsuariosController`; `RequirePermission` granular llega en UDT-006. ## Infra testing (deviations documentadas) - `IClassFixture<TestWebAppFactory>` → `ICollectionFixture<TestWebAppFactory>` (shared factory) — resuelve `ObjectDisposedException: RSABCrypt` cross-class. - `tests/tests.runsettings` con `MaxCpuCount=1` — evita race entre assemblies sobre `SIGCM2_Test`. - `SeedRolCanonicalAsync` post-Respawn en 4 fixtures — Respawn `TablesToIgnore` no respetado en 6.2.1. ## Test plan - [x] Login como admin. - [x] Navegar a `/admin/roles` — ver los 8 seeds con badges Activo/Inactivo. - [x] Crear rol nuevo (ej. `cajero_senior`) — validar regex codigo `^[a-z][a-z0-9_]*$`. - [x] Editar un rol — modificar nombre/descripción, togglear Activo. - [x] Desactivar un rol sin usuarios → 204. - [x] Intentar desactivar `admin` → 409 (tiene el usuario admin asociado). - [x] Ir a `/usuarios/nuevo` — confirmar que el dropdown de Rol se puebla async con los activos y NO incluye inactivos. - [x] Crear usuario con un rol canónico → 201. - [x] Crear usuario con rol inactivo (curl/Postman) → 400 con `errors.Rol`. ## Out of Scope (diferidos) - Permisos granulares y `RolPermisos` → **UDT-005**. - `RequirePermission` middleware → **UDT-006**. - Herencia de roles (`heredaDe`) → UDT-005. - Auditoría de cambios de roles → UDT futura. ## Artifacts SDD (engram) Audit trail completo en engram (topic_keys): - `sdd/udt-004-gestion-roles/{explore,proposal,spec,design,tasks,apply-progress,verify-report,archive-report}` - Observation IDs: #295 → #301 + archive report.
dmolinari added 4 commits 2026-04-15 16:10:13 +00:00
- Migraciones V003 (tabla Rol + 8 seeds canonicos) y V004 (drop CK + FK Usuario.Rol)
- Dominio: Rol entity + 3 excepciones (RolNotFound/AlreadyExists/InUse)
- Infraestructura: RolRepository (Dapper) con List/Get/ExistsActive/Add/Update/HasActiveUsuarios
- Application: CRUD queries y commands (List, Get, Create, Update, Deactivate) + validators (codigo regex ^[a-z][a-z0-9_]*$)
- Validator UDT-003: whitelist alineada a codigos canonicos (full IRolRepository lookup diferido a Phase 5.1)
- Tests: 169 application + 15 api (todos verdes). Respawn configurado para re-seedear Rol canonical post-reset.
- Estricto TDD: RED/GREEN/TRIANGULATE en todos los handlers nuevos.
- RolesController /api/v1/roles CRUD admin-only: GET list, GET {codigo}, POST, PUT, DELETE (soft-delete con guard 409)
- ExceptionFilter: mapea RolNotFound (404), RolAlreadyExists (409), RolInUse (409)
- DI: registra 5 handlers de Roles (Application) y IRolRepository/RolRepository (Infrastructure)
- CreateUsuarioCommandValidator: reemplaza whitelist hardcoded por IRolRepository.ExistsActiveByCodigoAsync via MustAsync; constructor recibe (AuthOptions, IRolRepository)
- Tests: 202 verdes (173 application + 29 api). Nuevas: RolesEndpointTests (13 integration), CreateUsuarioCommandValidatorTests reescrito con NSubstitute mock, CreateUsuario_WithInactiveRol_Returns400 en Api.Tests
- Fix: ApiIntegration pasa de IClassFixture (N factories) a ICollectionFixture (1 factory shared) — evitaba ObjectDisposedException sobre RSABCrypt al compartir coleccion con multiples test classes
- tests/tests.runsettings: MaxCpuCount=1 para evitar race entre assemblies sobre SIGCM2_Test
- features/roles: API clients (list/get/create/update/deactivate), TanStack Query hooks, RolForm (create + edit variants), RolesList con acciones y guard 409, paginas RolesPage/NewRolPage/EditRolPage
- router.tsx: rutas /admin/roles, /admin/roles/nuevo, /admin/roles/:codigo/editar
- AppSidebar: nav Roles (admin-only)
- features/users: useRolesForSelect wrapper (filtra activo=true), UserForm fetchea roles async con loading/error states; elimina ROL_OPTIONS hardcoded
- tests: 47 vitest verdes (10 authStore + 5 auth api + 7 axios + 3 useCreateUser + 3 RolesList + 5 LoginPage + 7 UserForm + 7 RolForm). Typecheck limpio
Reemplaza Assert.True(enumerable.Any(...)) por Assert.Contains idiomatico.
dmolinari merged commit e5ee8e673b into main 2026-04-15 16:19:58 +00:00
dmolinari deleted branch feature/UDT-004 2026-04-15 16:19:58 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#8