- lagoon: 202610090001_backend_users_metadata adds WinterCMS's nullable jsonable backend_users.metadata column (TEXT); rollback drops only it - cabana.BackendUser.Metadata and Subtitle read metadata.subtitle leniently (NULL, blank, null, [], invalid JSON and non-strings read as empty) - GET /auth/me returns subtitle; PUT /account edits the signed-in admin's own names, email and subtitle, and the password with current_password; any other key is a 422 and nothing is written - email: required, valid, 6-255 chars, unique against other admins' email and login ignoring case (soft-deleted included); a unique-index race is 422 - a password change sets tokens_valid_after, revokes the presented token and gives a cookie session a fresh cookie - swag annotation, regenerated admin.json and schema.d.ts; route inventories, CSRF walk (27) and OpenAPI conformance learn the route - SPA: My account item in the user menu opens /account (AccountView), which saves through PUT /account and updates the top bar via setCurrentUser - backend::lang.myaccount strings (en, pl), rebuilt boardwalk dist - cabana and lagoon READMEs, users-and-permissions and admin-spa docs
94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
package cabana_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
)
|
|
|
|
// accountStamp is a per-run suffix for logins and emails, so the shared
|
|
// harness database can hold every run's admins side by side.
|
|
func accountStamp() string {
|
|
return time.Now().Format("150405.000000000")
|
|
}
|
|
|
|
// putAccount sends PUT /account with a Bearer token.
|
|
func putAccount(t *testing.T, h http.Handler, token string, body any) *httpResult {
|
|
t.Helper()
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := postAuth(t, h, http.MethodPut, adminAPI("/account"), token, raw)
|
|
return &httpResult{Code: rec.Code, Body: rec.Body.Bytes(), Header: rec.Header()}
|
|
}
|
|
|
|
// httpResult is a recorded response.
|
|
type httpResult struct {
|
|
Code int
|
|
Body []byte
|
|
Header http.Header
|
|
}
|
|
|
|
// profileOf decodes an AdminProfile envelope.
|
|
func profileOf(t *testing.T, raw []byte) cabana.AdminProfile {
|
|
t.Helper()
|
|
var body cabana.Envelope[cabana.AdminProfile]
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
t.Fatalf("profile json: %v body=%s", err, raw)
|
|
}
|
|
return body.Data
|
|
}
|
|
|
|
// signIn logs an admin in over Bearer transport and returns the token.
|
|
func signIn(t *testing.T, h http.Handler, login string) string {
|
|
t.Helper()
|
|
rec := postJSON(t, h, adminAPI("/auth/login"), map[string]string{"login": login, "password": adminTestPassword})
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("login status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
return accessToken(t, rec.Body.Bytes())
|
|
}
|
|
|
|
func TestAccountProfileSmoke(t *testing.T) {
|
|
gdb := adminGorm(t)
|
|
h := adminHandler(t, gdb, nil)
|
|
stamp := accountStamp()
|
|
login := "acct-smoke-" + stamp
|
|
insertAdmin(t, gdb, login, login+"@example.test", adminTestPassword, true, false)
|
|
token := signIn(t, h, login)
|
|
|
|
res := putAccount(t, h, token, map[string]any{
|
|
"first_name": "Grace",
|
|
"last_name": "Hopper",
|
|
"email": "grace-" + stamp + "@example.test",
|
|
"subtitle": "Rear admiral",
|
|
})
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("put status=%d body=%s", res.Code, res.Body)
|
|
}
|
|
if got := profileOf(t, res.Body); got.FirstName != "Grace" || got.Subtitle != "Rear admiral" {
|
|
t.Fatalf("put profile = %+v", got)
|
|
}
|
|
me := getAuth(t, h, adminAPI("/auth/me"), token)
|
|
if me.Code != http.StatusOK {
|
|
t.Fatalf("me status=%d body=%s", me.Code, me.Body.String())
|
|
}
|
|
got := profileOf(t, me.Body.Bytes())
|
|
if got.FirstName != "Grace" || got.LastName != "Hopper" || got.Email != "grace-"+stamp+"@example.test" || got.Subtitle != "Rear admiral" || got.Login != login {
|
|
t.Fatalf("me profile = %+v", got)
|
|
}
|
|
|
|
refused := putAccount(t, h, token, map[string]any{"email": "grace-" + stamp + "@example.test", "is_superuser": true})
|
|
if refused.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("is_superuser status=%d body=%s", refused.Code, refused.Body)
|
|
}
|
|
var superuser bool
|
|
if err := gdb.Raw(`SELECT is_superuser FROM backend_users WHERE login = ?`, login).Scan(&superuser).Error; err != nil || superuser {
|
|
t.Fatalf("is_superuser=%v err=%v", superuser, err)
|
|
}
|
|
}
|