Auditoría de /portal/dashboard. Cuatro problemas, todos en el mismo bucle: 1. El avance se veía un render atrasado. ActualizarProgresoProyecto corría DESPUÉS de cargar los proyectos: escribía el valor nuevo en la base pero los structs ya cargados seguían con el viejo, que es lo que se renderiza. El usuario veía el cálculo de la visita anterior. 2. N+1 con escrituras en un GET: dos COUNT y un UPDATE por proyecto. Con 10 proyectos, 30 consultas y 10 escrituras por cada carga del dashboard — incluyendo las de cualquier bot que pase. Ahora es UNA consulta agrupada y ninguna escritura; los caminos que tocan una fase ya mantienen la columna al día, así que recalcular en el GET no aportaba nada. 3. Orden aleatorio de los grupos: se recorría un map de Go, así que un partner veía sus clientes en distinto orden en cada recarga. 4. isPartner se decidía con u.Rol mientras el alcance se decidía con Role.EsPortalPartner. Desincronizados, un usuario veía proyectos de varios clientes sin agrupar, o la vista agrupada vacía. Ahora hay una sola definición (PortalUser.EsPartner) con test de los dos sentidos. Además, el error de carga se descartaba con `_` y el usuario terminaba viendo "no tenés proyectos", indistinguible de una caída de la base. Sin hallazgos de seguridad: el chequeo de acceso por cliente está en todas las rutas, la sesión revalida Activo en cada request, y las plantillas no usan x-html ni template.HTML, así que Go escapa todo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
63 lines
2.1 KiB
Go
63 lines
2.1 KiB
Go
package models
|
|
|
|
import "testing"
|
|
|
|
// El rol vive en dos lados (la columna Rol y el flag EsPortalPartner del
|
|
// Role). Consultarlos por separado se contradecía: el dashboard elegía el
|
|
// layout con uno y los proyectos con el otro. EsPartner es ahora la única
|
|
// definición, y estos casos fijan cuál gana.
|
|
func TestEsPartner(t *testing.T) {
|
|
casos := []struct {
|
|
nombre string
|
|
user *PortalUser
|
|
esperado bool
|
|
}{
|
|
{"nil no es partner", nil, false},
|
|
{"sin Role, rol partner", &PortalUser{Rol: "partner"}, true},
|
|
{"sin Role, rol cliente", &PortalUser{Rol: "cliente"}, false},
|
|
{"con Role, el flag manda aunque el rol diga cliente",
|
|
&PortalUser{Rol: "cliente", Role: &Roles{EsPortalPartner: true}}, true},
|
|
{"con Role, el flag manda aunque el rol diga partner",
|
|
&PortalUser{Rol: "partner", Role: &Roles{EsPortalPartner: false}}, false},
|
|
}
|
|
for _, cas := range casos {
|
|
t.Run(cas.nombre, func(t *testing.T) {
|
|
if got := cas.user.EsPartner(); got != cas.esperado {
|
|
t.Errorf("EsPartner() = %v, esperaba %v", got, cas.esperado)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// El alcance del portal se deriva de EsPartner: un desajuste acá es que un
|
|
// usuario vea proyectos de un cliente que no le corresponde.
|
|
func TestGetClienteIDsForPortalUser(t *testing.T) {
|
|
cid := uint(7)
|
|
casos := []struct {
|
|
nombre string
|
|
user *PortalUser
|
|
esperado []uint
|
|
}{
|
|
{"nil no ve nada", nil, []uint{}},
|
|
{"cliente ve el suyo", &PortalUser{Rol: "cliente", ClienteID: &cid}, []uint{7}},
|
|
{"cliente sin ClienteID no ve nada", &PortalUser{Rol: "cliente"}, []uint{}},
|
|
{"partner ve los de sus accesos",
|
|
&PortalUser{Rol: "partner", PortalAccesos: []PortalAcceso{{ClienteID: 3}, {ClienteID: 9}}},
|
|
[]uint{3, 9}},
|
|
{"partner sin accesos no ve nada", &PortalUser{Rol: "partner"}, []uint{}},
|
|
}
|
|
for _, cas := range casos {
|
|
t.Run(cas.nombre, func(t *testing.T) {
|
|
got := GetClienteIDsForPortalUser(cas.user)
|
|
if len(got) != len(cas.esperado) {
|
|
t.Fatalf("= %v, esperaba %v", got, cas.esperado)
|
|
}
|
|
for i := range got {
|
|
if got[i] != cas.esperado[i] {
|
|
t.Errorf("= %v, esperaba %v", got, cas.esperado)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|