test(cabana): cover the Roles form's holders list (quick-261028-rqg)

- TestRolesHoldersPartial (Docker-free): users partial field, view model
  guard, links, state lines, escaped strings, empty line, note
- TestRolesHolders (Postgres): D-03 hiding, links by manage_users, states,
  empty role, 100-row cap with note, 403 without manage_roles
This commit is contained in:
Jakub Zych
2026-10-28 21:37:22 +01:00
parent 8cb56a4c51
commit 653d95a5b9
3 changed files with 178 additions and 0 deletions

View File

@@ -336,3 +336,96 @@ func (e *screensEnv) reloadRoleByCode(t *testing.T, code string) cabana.BackendU
}
return role
}
// TestRolesHolders: the Roles update form's users partial lists the role's
// holders in the editor's D-03 scope, marks their states, links logins only
// for editors holding backend.manage_users, shows the empty line, caps the
// rows at 100 with a note and is closed to editors without
// backend.manage_roles.
func TestRolesHolders(t *testing.T) {
env := newScreensEnv(t)
role := env.seedRole(t, roleSeed{Name: "holders"})
active := env.seedAdmin(t, adminSeed{Name: "holder-active", Role: role.ID})
inactive := env.seedAdmin(t, adminSeed{Name: "holder-inactive", Role: role.ID, Inactive: true})
deleted := env.seedAdmin(t, adminSeed{Name: "holder-deleted", Role: role.ID, Deleted: true})
super := env.seedAdmin(t, adminSeed{Name: "holder-super", Role: role.ID, Superuser: true})
other := env.seedAdmin(t, adminSeed{Name: "holder-other"})
holders := []cabana.BackendUser{active, inactive, deleted, super}
superEditor := env.seedAdmin(t, adminSeed{Name: "holders-super-editor", Superuser: true})
rolesOnly := env.seedAdmin(t, adminSeed{Name: "holders-roles-only",
RolePermissions: map[string]int{cabana.PermissionManageRoles: 1}})
rolesAndUsers := env.seedAdmin(t, adminSeed{Name: "holders-roles-users",
RolePermissions: map[string]int{cabana.PermissionManageRoles: 1, cabana.PermissionManageUsers: 1}})
outsider := env.seedAdmin(t, adminSeed{Name: "holders-outsider"})
partial := func(roleID uint, token string) string {
t.Helper()
body := env.expect(t, http.StatusOK, http.MethodGet, fmt.Sprintf("%s/partials/users?id=%d", rolesPath, roleID), token, nil)
return fmt.Sprint(body["data"])
}
link := func(id uint) string { return fmt.Sprintf("/backend/summercms/cabana/users/%d/preview", id) }
// A superuser editor sees every holder, superusers included, with
// their states and preview links; an admin of another role is absent.
text := partial(role.ID, env.login(t, superEditor.Login))
for _, holder := range holders {
if !strings.Contains(text, holder.Login) || !strings.Contains(text, link(holder.ID)) {
t.Fatalf("superuser editor: holder %s (link %s) missing: %s", holder.Login, link(holder.ID), text)
}
}
for _, state := range []string{"Superuser", "Deactivated", "Deleted"} {
if !strings.Contains(text, "text:"+state) {
t.Fatalf("superuser editor: state %s missing: %s", state, text)
}
}
if strings.Contains(text, other.Login) {
t.Fatalf("superuser editor: admin of another role listed: %s", text)
}
// D-03: a non-superuser editor never sees the superuser holder, and
// without backend.manage_users gets no links.
text = partial(role.ID, env.login(t, rolesOnly.Login))
if strings.Contains(text, super.Login) || strings.Contains(text, "Superuser") {
t.Fatalf("D-03: superuser holder shown to a non-superuser editor: %s", text)
}
if !strings.Contains(text, active.Login) || !strings.Contains(text, deleted.Login) || !strings.Contains(text, inactive.Login) {
t.Fatalf("manage_roles editor: holders missing: %s", text)
}
if strings.Contains(text, "/summercms/cabana/users/") {
t.Fatalf("manage_roles editor without manage_users got preview links: %s", text)
}
text = partial(role.ID, env.login(t, rolesAndUsers.Login))
if strings.Contains(text, super.Login) {
t.Fatalf("D-03: superuser holder shown to a non-superuser editor: %s", text)
}
if !strings.Contains(text, link(active.ID)) {
t.Fatalf("manage_users editor: preview link missing: %s", text)
}
// A role nobody holds shows the empty line.
superToken := env.login(t, superEditor.Login)
empty := env.seedRole(t, roleSeed{Name: "holders-empty"})
if text := partial(empty.ID, superToken); !strings.Contains(text, "No administrator has this role.") {
t.Fatalf("empty role: %s", text)
}
// Without backend.manage_roles the partial route is denied.
env.expectRefusal(t, http.StatusForbidden, http.MethodGet, fmt.Sprintf("%s/partials/users?id=%d", rolesPath, role.ID),
env.login(t, outsider.Login), nil, "")
// 101 holders: the first 100 are listed, with the note naming the total.
crowded := env.seedRole(t, roleSeed{Name: "holders-crowded"})
prefix := "crowd-" + env.stamp + "-"
for i := 0; i < 101; i++ {
env.seedAdmin(t, adminSeed{Name: "crowd", Role: crowded.ID})
}
text = partial(crowded.ID, superToken)
// Each listed login appears twice: as the login and in the email.
if got := strings.Count(text, prefix); got != 200 {
t.Fatalf("crowded role: %d login occurrences, want 200 (100 rows): %s", got, text)
}
if !strings.Contains(text, "Showing the first 100 of 101 administrators") {
t.Fatalf("crowded role: note missing: %s", text)
}
}