- golem15.users.manage_backend_links (no default role), en and pl labels - record actions on summercms.cabana.users: link_frontend_account asks for the frontend user's email (exact, case-insensitive, active users only), records linked_by and fires the link events; unlink_frontend_account (danger) removes the link - a taken or unknown account answers 422 on the prompt
152 lines
6.4 KiB
Go
152 lines
6.4 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/classes"
|
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
const permManageBackendLinks = "golem15.users.manage_backend_links"
|
|
|
|
// administratorPath is the admin API path of one administrator on the
|
|
// Administrators screen, with an optional suffix.
|
|
func administratorPath(id uint, suffix string) string {
|
|
return adminAPI("/summercms/cabana/users/" + strconv.FormatUint(uint64(id), 10) + suffix)
|
|
}
|
|
|
|
// superAdmin is admin with the superuser flag set.
|
|
func (e *adminEnv) superAdmin(t *testing.T) string {
|
|
t.Helper()
|
|
token := e.admin(t)
|
|
if err := e.gdb.Exec(`UPDATE backend_users SET is_superuser = TRUE WHERE id = ?`, adminID(t, token)).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
// administratorActions returns the record actions the Administrators screen
|
|
// offers on an administrator.
|
|
func (e *adminEnv) administratorActions(t *testing.T, token string, id uint) []string {
|
|
t.Helper()
|
|
rec := e.expect(t, http.StatusOK, http.MethodGet, administratorPath(id, ""), token, nil)
|
|
var meta struct {
|
|
Actions []struct {
|
|
Name string `json:"name"`
|
|
} `json:"actions"`
|
|
}
|
|
if err := json.Unmarshal(adminDecode(t, rec).Meta, &meta); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := []string{}
|
|
for _, action := range meta.Actions {
|
|
if strings.HasSuffix(action.Name, "frontend_account") {
|
|
names = append(names, action.Name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// TestBackendLinkAdminSmoke: on the Administrators screen an administrator
|
|
// with golem15.users.manage_backend_links links a frontend account to
|
|
// another administrator by its exact email (any letter case) and unlinks
|
|
// it; linked_by records the caller, the event fires, a taken or unknown
|
|
// account is a 422 on the prompt, and an administrator without the
|
|
// permission is neither offered the actions nor allowed to run them.
|
|
func TestBackendLinkAdminSmoke(t *testing.T) {
|
|
env := newAdminEnv(t)
|
|
var mu sync.Mutex
|
|
var linked []models.BackendLink
|
|
env.app.Events.Listen("test.backend_linked", func(_ context.Context, e *classes.BackendLinkedEvent) error {
|
|
mu.Lock()
|
|
linked = append(linked, *e.Link)
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
stamp := adminStamp()
|
|
editor := env.seedUser(t, "editor", func(u *models.User) { u.Email = "editor-" + stamp + "@example.test" })
|
|
second := env.seedUser(t, "second", nil)
|
|
targetToken := env.admin(t)
|
|
target := adminID(t, targetToken)
|
|
super := env.superAdmin(t)
|
|
superID := adminID(t, super)
|
|
resolver, _ := env.app.Lookup[pact.MCPLinkedBackendResolver]()
|
|
|
|
if got := env.administratorActions(t, super, target); !slices.Equal(got, []string{"link_frontend_account"}) {
|
|
t.Fatalf("offered before the link = %v", got)
|
|
}
|
|
link := func(token, value string) *http.Response {
|
|
t.Helper()
|
|
rec := env.call(t, http.MethodPost, administratorPath(target, "/actions/link_frontend_account"), token, map[string]any{"payload": value})
|
|
return rec.Result()
|
|
}
|
|
|
|
// Unknown email: 422 on payload with the unknown_user text.
|
|
rec := env.call(t, http.MethodPost, administratorPath(target, "/actions/link_frontend_account"), super, map[string]any{"payload": "nobody-" + stamp + "@example.test"})
|
|
if rec.Code != http.StatusUnprocessableEntity || !slices.Contains(adminDecode(t, rec).Error.Details["payload"], "No active frontend user has this email.") {
|
|
t.Fatalf("unknown email = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// The email matches whatever its letter case.
|
|
if res := link(super, " Editor-"+stamp+"@Example.TEST "); res.StatusCode != http.StatusOK {
|
|
t.Fatalf("set = %d", res.StatusCode)
|
|
}
|
|
var row models.BackendLink
|
|
if err := env.gdb.Where("backend_user_id = ?", target).Take(&row).Error; err != nil || row.UserID != editor.ID || row.LinkedBy == nil || *row.LinkedBy != superID {
|
|
t.Fatalf("link row = %+v %v", row, err)
|
|
}
|
|
if got, found, err := resolver.LinkedBackendUser(context.Background(), editor.ID); err != nil || !found || got.ID != target {
|
|
t.Fatalf("LinkedBackendUser = %+v %v %v", got, found, err)
|
|
}
|
|
mu.Lock()
|
|
if len(linked) != 1 || linked[0].UserID != editor.ID || linked[0].BackendUserID != target {
|
|
t.Fatalf("BackendLinkedEvent saw %+v", linked)
|
|
}
|
|
mu.Unlock()
|
|
if got := env.administratorActions(t, super, target); !slices.Equal(got, []string{"unlink_frontend_account"}) {
|
|
t.Fatalf("offered after the link = %v", got)
|
|
}
|
|
|
|
// The administrator is linked: a second set no longer applies (409),
|
|
// and the same frontend user cannot be linked to another administrator.
|
|
if res := link(super, second.Email); res.StatusCode != http.StatusConflict {
|
|
t.Fatalf("second set on a linked administrator = %d", res.StatusCode)
|
|
}
|
|
otherToken := env.admin(t)
|
|
other := adminID(t, otherToken)
|
|
rec = env.call(t, http.MethodPost, administratorPath(other, "/actions/link_frontend_account"), super, map[string]any{"payload": editor.Email})
|
|
if rec.Code != http.StatusUnprocessableEntity || !slices.Contains(adminDecode(t, rec).Error.Details["payload"], "This administrator or this frontend user is already linked.") {
|
|
t.Fatalf("frontend user linked twice = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Without the permission: not offered, and refused.
|
|
limited := env.admin(t, cabana.PermissionManageUsers)
|
|
if got := env.administratorActions(t, limited, target); len(got) != 0 {
|
|
t.Fatalf("limited administrator is offered %v", got)
|
|
}
|
|
env.expect(t, http.StatusForbidden, http.MethodPost, administratorPath(target, "/actions/unlink_frontend_account"), limited, map[string]any{})
|
|
env.expect(t, http.StatusForbidden, http.MethodPost, administratorPath(other, "/actions/link_frontend_account"), limited, map[string]any{"payload": second.Email})
|
|
manager := env.admin(t, cabana.PermissionManageUsers, permManageBackendLinks)
|
|
if got := env.administratorActions(t, manager, target); !slices.Equal(got, []string{"unlink_frontend_account"}) {
|
|
t.Fatalf("manager is offered %v", got)
|
|
}
|
|
|
|
// Unlink.
|
|
env.expect(t, http.StatusOK, http.MethodPost, administratorPath(target, "/actions/unlink_frontend_account"), manager, map[string]any{})
|
|
if _, found, err := resolver.LinkedBackendUser(context.Background(), editor.ID); err != nil || found {
|
|
t.Fatalf("after unlink found=%v err=%v", found, err)
|
|
}
|
|
if got := env.administratorActions(t, super, target); !slices.Equal(got, []string{"link_frontend_account"}) {
|
|
t.Fatalf("offered after the unlink = %v", got)
|
|
}
|
|
}
|