feat: roles SPA Users, Profile, API keys, Activity
This commit is contained in:
1 parent
3db490a615
commit
34dffe222f
98 files changed
+4957
-434
No files matched your search
@@ -59,7 +59,7 @@ func newAcceptInviteFixture(t *testing.T, maxAttempts int) *acceptInviteFixture
|
||||
f.token,
|
||||
true,
|
||||
)
|
||||
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()))
|
||||
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()))
|
||||
listHandler := userhandler.NewInviteHandler(
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, newSeededMockRoleRepo(), newMockContentTypeRepo(), f.hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
nil,
|
||||
|
||||
@@ -77,7 +77,7 @@ func newActivityHTTPEnv(t *testing.T, attempts int) *activityHTTPEnv {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(e.users),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(e.roles),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(e.roles),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(e.roles, e.users),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(e.users, hasher, h.recorder),
|
||||
RequestPasswordResetUseCase: useCases.NewRequestPasswordResetUseCase(e.users, hasher, e.mail, "https://cms.example.com", h.recorder),
|
||||
ResetPasswordUseCase: useCases.NewResetPasswordUseCase(e.users, hasher, e.keys, resetClock, h.recorder),
|
||||
|
||||
@@ -116,7 +116,7 @@ func newAPIKeyEnvRecording(t *testing.T, recorder audit.Recorder) *apiKeyEnv {
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(env.articles, recorder),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(env.roles),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(env.roles, env.users),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(env.roles),
|
||||
ListPermissionsUseCase: useCases.NewListPermissionsUseCase(),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(env.users, newSeededMockRoleRepo(), hasher, mail, "https://cms.example.com", recorder),
|
||||
|
||||
@@ -59,7 +59,7 @@ func newContainerHarness(t *testing.T, container bool, helperInstalled bool) *co
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(newUpdateUserRepo()),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, helper, rig.results, rig.requests, updateRunning, container, now),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now, discardRecorder),
|
||||
StartUpdateDownloadUseCase: start,
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
meHandler := auth.NewMeHandler(getUserUseCase, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()))
|
||||
meHandler := auth.NewMeHandler(getUserUseCase, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()))
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
|
||||
@@ -115,7 +115,7 @@ func (e *variantEnv) container(limits useCases.ServeVariantLimits) *api.UseCases
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(e.users),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
OpenMediaUseCase: useCases.NewOpenMediaUseCase(e.storage),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(e.images, e.storage, e.files, recorder, plainSites(), plainAuthors(), e.articles),
|
||||
MergeMediaUseCase: newTestMergeMedia(e.images, e.articles, e.storage, e.files, recorder, plainSites(), plainAuthors(), newMockContentTypeRepo(), newMockEntryRepo()),
|
||||
|
||||
@@ -496,7 +496,7 @@ func TestInvitationsHttp(t *testing.T) {
|
||||
if want.kind == "invite" {
|
||||
assertExactKeys(t, item, "id", "kind", "email", "role", "status", "created_at")
|
||||
} else {
|
||||
assertExactKeys(t, item, "id", "kind", "username", "display_name", "email", "role", "status", "created_at")
|
||||
assertExactKeys(t, item, "id", "kind", "username", "display_name", "email", "role", "role_assigned_by", "status", "created_at")
|
||||
}
|
||||
}
|
||||
if decodedString(t, items[2], "display_name") != "Ines Duarte" {
|
||||
|
||||
@@ -75,7 +75,7 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images, env.storage, discardRecorder, imaging.NewMetadataStripper()),
|
||||
GenerateVariantsOnUploadUseCase: newInertGenerateOnUploadUseCase(),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(env.images, env.articles, env.users, env.variants, plainSites(), plainAuthors(), newMockContentTypeRepo(), newMockEntryRepo()),
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/publicuser"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/user"
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const rabyKey = "role_assigned_by"
|
||||
|
||||
func rabySeedAssigner(w *usersAPIWorld, target uint32, assigner uint32) {
|
||||
member := w.users.users[target]
|
||||
member.RoleAssignedByID = assigner
|
||||
w.users.users[target] = member
|
||||
}
|
||||
|
||||
func rabyAssignerOf(t *testing.T, rec *httptest.ResponseRecorder, container string) any {
|
||||
t.Helper()
|
||||
body := usersAPIDecode(t, rec)
|
||||
if container != "" {
|
||||
body = body[container].(map[string]any)
|
||||
}
|
||||
value, present := body[rabyKey]
|
||||
if !present {
|
||||
t.Fatalf("the %s key must always be present: %q", rabyKey, rec.Body.String())
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func rabyWant(id float64, name string) map[string]any {
|
||||
return map[string]any{"id": id, "name": name}
|
||||
}
|
||||
|
||||
func rabyAcceptKim(t *testing.T, w *usersAPIWorld) (user.User, *httptest.ResponseRecorder) {
|
||||
t.Helper()
|
||||
rec := w.anonymous(t, http.MethodPost, "/v1/auth/invite/accept", `{"code": "code3", "username": "kim", "password": "password1"}`)
|
||||
if rec.Code >= 300 {
|
||||
t.Fatalf("the accept must succeed, got %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
for _, member := range w.users.users {
|
||||
if member.Username == "kim" {
|
||||
return member, rec
|
||||
}
|
||||
}
|
||||
t.Fatalf("kim was not created")
|
||||
return user.User{}, rec
|
||||
}
|
||||
|
||||
func TestRoleAssignedByIsSetOnChangeRole(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rec := w.putRole(t, uaAdmin, "41", w.idText("Writer"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := rabyAssignerOf(t, rec, ""); !reflect.DeepEqual(got, rabyWant(float64(uaAdmin), "jonas")) {
|
||||
t.Errorf("the answering item must carry the caller, got %v", got)
|
||||
}
|
||||
if w.users.users[41].RoleAssignedByID != uaAdmin {
|
||||
t.Errorf("expected 5 stored, got %d", w.users.users[41].RoleAssignedByID)
|
||||
}
|
||||
me := w.do(t, 41, http.MethodGet, "/v1/auth/me", "")
|
||||
if got := rabyAssignerOf(t, me, "access"); !reflect.DeepEqual(got, rabyWant(float64(uaAdmin), "jonas")) {
|
||||
t.Errorf("unexpected access.role_assigned_by %v", got)
|
||||
}
|
||||
if rec := w.putRole(t, uaOwner, "41", w.idText("Intern")); rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d", rec.Code)
|
||||
}
|
||||
if w.users.users[41].RoleAssignedByID != uaOwner {
|
||||
t.Errorf("expected 3 stored, got %d", w.users.users[41].RoleAssignedByID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByUnchangedOnNoOp(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, 41, uaOwner)
|
||||
w.users.updateRoleCalls = 0
|
||||
rec := w.putRole(t, uaAdmin, "41", w.idText("Contributor"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
if w.users.updateRoleCalls != 0 {
|
||||
t.Errorf("a no-op change must not write")
|
||||
}
|
||||
if w.users.users[41].RoleAssignedByID != uaOwner {
|
||||
t.Errorf("the assigner must stay 3, got %d", w.users.users[41].RoleAssignedByID)
|
||||
}
|
||||
if got := rabyAssignerOf(t, rec, ""); !reflect.DeepEqual(got, rabyWant(float64(uaOwner), "marta")) {
|
||||
t.Errorf("the item must show the existing assigner, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByUnchangedOnRefusal(t *testing.T) {
|
||||
rows := []struct {
|
||||
name string
|
||||
caller uint32
|
||||
target string
|
||||
role func(w *usersAPIWorld) string
|
||||
hook func(w *usersAPIWorld)
|
||||
status int
|
||||
}{
|
||||
{"role_locked", uaAdmin, "3", func(w *usersAPIWorld) string { return w.idText("Writer") }, nil, http.StatusForbidden},
|
||||
{"own_role", uaAdmin, "5", func(w *usersAPIWorld) string { return w.idText("Writer") }, nil, http.StatusForbidden},
|
||||
{"escalation", uaPeople, "41", func(w *usersAPIWorld) string { return w.idText("Admin") }, nil, http.StatusForbidden},
|
||||
{"user_not_found", uaAdmin, "999", func(w *usersAPIWorld) string { return w.idText("Writer") }, nil, http.StatusNotFound},
|
||||
{"invalid_role", uaAdmin, "41", func(*usersAPIWorld) string { return "9999" }, nil, http.StatusBadRequest},
|
||||
{"user_role_changed", uaAdmin, "41", func(w *usersAPIWorld) string { return w.idText("Writer") }, func(w *usersAPIWorld) {
|
||||
w.users.beforeUpdateRole = func() {
|
||||
member := w.users.users[41]
|
||||
member.RoleID = w.id("Intern")
|
||||
w.users.users[41] = member
|
||||
}
|
||||
}, http.StatusConflict},
|
||||
}
|
||||
for _, row := range rows {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
for _, id := range []uint32{3, 5, 41} {
|
||||
rabySeedAssigner(w, id, uaViewer)
|
||||
}
|
||||
if row.hook != nil {
|
||||
row.hook(w)
|
||||
}
|
||||
rec := w.putRole(t, row.caller, row.target, row.role(w))
|
||||
if rec.Code != row.status {
|
||||
t.Fatalf("expected %d, got %d %q", row.status, rec.Code, rec.Body.String())
|
||||
}
|
||||
for _, id := range []uint32{3, 5, 41} {
|
||||
if w.users.users[id].RoleAssignedByID != uaViewer {
|
||||
t.Errorf("user %d: the assigner must not change, got %d", id, w.users.users[id].RoleAssignedByID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByAtAcceptInvite(t *testing.T) {
|
||||
t.Run("the inviter becomes the assigner", func(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
kim, rec := rabyAcceptKim(t, w)
|
||||
if kim.RoleAssignedByID != uaAdmin {
|
||||
t.Errorf("expected 5, got %d", kim.RoleAssignedByID)
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), rabyKey) {
|
||||
t.Errorf("the accept body must not carry the key: %q", rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a missing inviter gives NULL and still succeeds", func(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
delete(w.users.users, uaAdmin)
|
||||
kim, _ := rabyAcceptKim(t, w)
|
||||
if kim.RoleAssignedByID != 0 {
|
||||
t.Errorf("expected NULL, got %d", kim.RoleAssignedByID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAssignedByAtTransfer(t *testing.T) {
|
||||
transfer := func(t *testing.T, w *usersAPIWorld) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
return w.do(t, uaOwner, http.MethodPost, "/v1/site/transfer-ownership", w.transferBody(uaAdmin, "jonas@renew.example"))
|
||||
}
|
||||
|
||||
t.Run("the new owner is assigned by the former owner and the former owner is cleared", func(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, uaOwner, uaViewer)
|
||||
rabySeedAssigner(w, uaAdmin, uaViewer)
|
||||
w.users.transferCalls = 0
|
||||
if rec := transfer(t, w); rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
if w.users.transferCalls != 1 || w.users.users[uaAdmin].RoleAssignedByID != uaOwner || w.users.users[uaOwner].RoleAssignedByID != 0 {
|
||||
t.Errorf("unexpected assigners %d and %d after %d transaction(s)", w.users.users[uaAdmin].RoleAssignedByID, w.users.users[uaOwner].RoleAssignedByID, w.users.transferCalls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a failed or stale transaction changes neither", func(t *testing.T) {
|
||||
for name, arrange := range map[string]func(w *usersAPIWorld){
|
||||
"failure": func(w *usersAPIWorld) { w.users.fail.failNext("TransferOwnership") },
|
||||
"stale from": func(w *usersAPIWorld) {
|
||||
w.users.beforeTransfer = func() { rabySeedRole(w, uaOwner, w.id("Admin")) }
|
||||
},
|
||||
} {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, uaOwner, uaViewer)
|
||||
rabySeedAssigner(w, uaAdmin, uaViewer)
|
||||
arrange(w)
|
||||
if rec := transfer(t, w); rec.Code < 400 {
|
||||
t.Fatalf("%s: expected an error, got %d", name, rec.Code)
|
||||
}
|
||||
if w.users.users[uaAdmin].RoleAssignedByID != uaViewer || w.users.users[uaOwner].RoleAssignedByID != uaViewer {
|
||||
t.Errorf("%s: no assigner may change", name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func rabySeedRole(w *usersAPIWorld, id uint32, roleID uint32) {
|
||||
member := w.users.users[id]
|
||||
member.RoleID = roleID
|
||||
w.users.users[id] = member
|
||||
}
|
||||
|
||||
func TestRoleAssignedByAtRegister(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
w.users.users = map[uint32]user.User{}
|
||||
rec := w.anonymous(t, http.MethodPost, "/v1/auth/register", `{"username": "first", "password": "password1", "email": "first@renew.example"}`)
|
||||
if rec.Code >= 300 || len(w.users.users) != 1 {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
for _, member := range w.users.users {
|
||||
if member.RoleAssignedByID != 0 {
|
||||
t.Errorf("the first user has no assigner, got %d", member.RoleAssignedByID)
|
||||
}
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), rabyKey) {
|
||||
t.Errorf("the register body must not carry the key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByUnchangedOnReassign(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, 41, uaOwner)
|
||||
rec := w.do(t, uaAdmin, http.MethodPost, "/v1/role/"+w.idText("Contributor")+"/reassign", `{"target_role_id": `+w.idText("Intern")+`}`)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
if w.users.users[41].RoleID != w.id("Intern") || w.users.users[41].RoleAssignedByID != uaOwner {
|
||||
t.Errorf("41 must move and keep its assigner, got %+v", w.users.users[41])
|
||||
}
|
||||
}
|
||||
|
||||
func rabyList(t *testing.T, w *usersAPIWorld, caller uint32) []map[string]any {
|
||||
t.Helper()
|
||||
rec := w.do(t, caller, http.MethodGet, "/v1/user", "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
var items []map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func TestUserListCarriesRoleAssignedBy(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, 41, uaAdmin)
|
||||
items := rabyList(t, w, uaAdmin)
|
||||
seenUsers, seenInvites := 0, 0
|
||||
for _, item := range items {
|
||||
if item["kind"] == "invite" {
|
||||
seenInvites++
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "email", "role", "status", "created_at")
|
||||
continue
|
||||
}
|
||||
seenUsers++
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "username", "display_name", "email", "role", rabyKey, "status", "created_at")
|
||||
switch item["id"] {
|
||||
case float64(41):
|
||||
if !reflect.DeepEqual(item[rabyKey], rabyWant(float64(uaAdmin), "jonas")) {
|
||||
t.Errorf("user 41 must show its assigner, got %v", item[rabyKey])
|
||||
}
|
||||
default:
|
||||
if item[rabyKey] != nil {
|
||||
t.Errorf("user %v must show null, got %v", item["id"], item[rabyKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
if seenUsers == 0 || seenInvites == 0 {
|
||||
t.Fatalf("expected both kinds, got %d users and %d invites", seenUsers, seenInvites)
|
||||
}
|
||||
|
||||
t.Run("no per-row lookup", func(t *testing.T) {
|
||||
quiet := newUsersAPIWorld(t)
|
||||
quiet.users.getCalls = 0
|
||||
rabyList(t, quiet, uaAdmin)
|
||||
baseline := quiet.users.getCalls
|
||||
|
||||
busy := newUsersAPIWorld(t)
|
||||
for id := range busy.users.users {
|
||||
rabySeedAssigner(busy, id, uaAdmin)
|
||||
}
|
||||
busy.users.getCalls = 0
|
||||
rabyList(t, busy, uaAdmin)
|
||||
if busy.users.getCalls != baseline {
|
||||
t.Errorf("expected %d Get calls like the list without assigners, got %d", baseline, busy.users.getCalls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a vanished assigner is null", func(t *testing.T) {
|
||||
gone := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(gone, 41, 777)
|
||||
for _, item := range rabyList(t, gone, uaAdmin) {
|
||||
if item["id"] == float64(41) && item[rabyKey] != nil {
|
||||
t.Errorf("expected null, got %v", item[rabyKey])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAssignedByFollowsRename(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, 41, uaAdmin)
|
||||
renamed := w.users.users[uaAdmin]
|
||||
renamed.DisplayName = "Jonas W."
|
||||
w.users.users[uaAdmin] = renamed
|
||||
me := w.do(t, 41, http.MethodGet, "/v1/auth/me", "")
|
||||
if got := rabyAssignerOf(t, me, "access"); !reflect.DeepEqual(got, rabyWant(float64(uaAdmin), "Jonas W.")) {
|
||||
t.Errorf("expected the new name, got %v", got)
|
||||
}
|
||||
before := usersAPIDecode(t, me)
|
||||
delete(w.users.users, uaAdmin)
|
||||
me = w.do(t, 41, http.MethodGet, "/v1/auth/me", "")
|
||||
if me.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d", me.Code)
|
||||
}
|
||||
if got := rabyAssignerOf(t, me, "access"); got != nil {
|
||||
t.Errorf("a removed assigner must read null, got %v", got)
|
||||
}
|
||||
after := usersAPIDecode(t, me)
|
||||
if !reflect.DeepEqual(before["access"].(map[string]any)["role"], after["access"].(map[string]any)["role"]) {
|
||||
t.Errorf("nothing else about the response may change")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMeKeepsOtherShapes(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, 41, uaAdmin)
|
||||
keys := func(value any) []string {
|
||||
raw, _ := json.Marshal(value)
|
||||
var object map[string]any
|
||||
_ = json.Unmarshal(raw, &object)
|
||||
names := []string{}
|
||||
for key := range object {
|
||||
names = append(names, key)
|
||||
}
|
||||
return names
|
||||
}
|
||||
for _, name := range keys(publicuser.New(w.users.users[41])) {
|
||||
if name == rabyKey {
|
||||
t.Errorf("PublicUser must not carry %s", rabyKey)
|
||||
}
|
||||
}
|
||||
login := w.anonymous(t, http.MethodPost, "/v1/auth/login", `{"username": "user41", "password": "password1"}`)
|
||||
verify := w.anonymous(t, http.MethodPost, "/v1/auth/verify", `{"code": "x"}`)
|
||||
for name, rec := range map[string]*httptest.ResponseRecorder{"login": login, "verify": verify} {
|
||||
if strings.Contains(rec.Body.String(), rabyKey) {
|
||||
t.Errorf("%s must not carry the key: %q", name, rec.Body.String())
|
||||
}
|
||||
}
|
||||
me := usersAPIDecode(t, w.do(t, 41, http.MethodGet, "/v1/auth/me", ""))
|
||||
if _, leaked := me[rabyKey]; leaked {
|
||||
t.Errorf("the key lives under access only")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByNoNewGate(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
rabySeedAssigner(w, 41, uaAdmin)
|
||||
for _, item := range rabyList(t, w, uaViewer) {
|
||||
if item["kind"] == "user" {
|
||||
if _, present := item[rabyKey]; !present {
|
||||
t.Errorf("a users.view caller must read the key")
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, caller := range []uint32{uaWriter, uaInviter, 41} {
|
||||
me := w.do(t, caller, http.MethodGet, "/v1/auth/me", "")
|
||||
if me.Code != http.StatusOK {
|
||||
t.Fatalf("user %d: unexpected %d", caller, me.Code)
|
||||
}
|
||||
rabyAssignerOf(t, me, "access")
|
||||
}
|
||||
for _, path := range []string{"/v1/user", "/v1/auth/me"} {
|
||||
if rec := w.anonymous(t, http.MethodGet, path, ""); rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("%s: expected 401, got %d", path, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByPersistence(t *testing.T) {
|
||||
t.Run("UpdateRole writes the assigner with the compare-and-swap", func(t *testing.T) {
|
||||
db := openMigratedRolesDB(t)
|
||||
repo := persistence.NewUserRepository(db)
|
||||
admin, writer := storedRole(t, db, "Admin").ID, storedRole(t, db, "Writer").ID
|
||||
uaPersistedUser(t, db, 5, uaRoleIDPtr(admin), true)
|
||||
updated, err := repo.UpdateRole(5, admin, writer, 3)
|
||||
if err != nil || updated.RoleAssignedByID != 3 || userRow(t, db, 5).RoleAssignedByID == nil || *userRow(t, db, 5).RoleAssignedByID != 3 {
|
||||
t.Fatalf("unexpected %+v, %v", updated, err)
|
||||
}
|
||||
if _, err := repo.UpdateRole(5, admin, writer, 9); err == nil || *userRow(t, db, 5).RoleAssignedByID != 3 {
|
||||
t.Errorf("a stale swap must change nothing, got %v", err)
|
||||
}
|
||||
if _, err := repo.UpdateRole(5, writer, admin, 0); err != nil || userRow(t, db, 5).RoleAssignedByID != nil {
|
||||
t.Errorf("a zero assigner must store NULL, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TransferOwnership sets and clears the assigners in one transaction", func(t *testing.T) {
|
||||
db := openMigratedRolesDB(t)
|
||||
repo := persistence.NewUserRepository(db)
|
||||
owner, admin := storedRole(t, db, "Owner").ID, storedRole(t, db, "Admin").ID
|
||||
uaPersistedUser(t, db, 3, uaRoleIDPtr(owner), true)
|
||||
uaPersistedUser(t, db, 5, uaRoleIDPtr(admin), true)
|
||||
uaPersistedUser(t, db, 12, uaRoleIDPtr(admin), false)
|
||||
db.Exec("UPDATE users SET role_assigned_by_id = 12 WHERE id = 3")
|
||||
if err := repo.TransferOwnership(3, 12, owner, admin); err == nil {
|
||||
t.Fatal("an unverified successor must be refused")
|
||||
}
|
||||
if row := userRow(t, db, 3); row.RoleAssignedByID == nil || *row.RoleAssignedByID != 12 || roleIDOf(row) != owner {
|
||||
t.Errorf("a refused transfer must change nothing, got %+v", row)
|
||||
}
|
||||
if err := repo.TransferOwnership(3, 5, owner, admin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if row := userRow(t, db, 3); row.RoleAssignedByID != nil {
|
||||
t.Errorf("the former owner must read NULL")
|
||||
}
|
||||
if row := userRow(t, db, 5); row.RoleAssignedByID == nil || *row.RoleAssignedByID != 3 {
|
||||
t.Errorf("the new owner must be assigned by 3")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AcceptWithUser stores the assigner and Create leaves NULL", func(t *testing.T) {
|
||||
db := openMigratedRolesDB(t)
|
||||
admin := storedRole(t, db, "Admin").ID
|
||||
row := uaPersistedInvite(t, db, "kim@x.example", uaRoleIDPtr(admin), false, false)
|
||||
created, err := persistence.NewInviteRepository(db).AcceptWithUser(row.ID, time.Now(), user.User{Username: "kim", Email: "kim@x.example", Password: "x", IsVerified: true, RoleID: admin, RoleAssignedByID: 5})
|
||||
if err != nil || created.RoleAssignedByID != 5 || *userRow(t, db, created.ID).RoleAssignedByID != 5 {
|
||||
t.Fatalf("unexpected %+v, %v", created, err)
|
||||
}
|
||||
bare, err := persistence.NewUserRepository(db).Create(user.User{Username: "b", Email: "b@x.example", Password: "x"})
|
||||
if err != nil || userRow(t, db, bare.ID).RoleAssignedByID != nil {
|
||||
t.Errorf("Create must leave NULL, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bulk reassign leaves the assigner", func(t *testing.T) {
|
||||
db := openMigratedRolesDB(t)
|
||||
admin, writer := storedRole(t, db, "Admin").ID, storedRole(t, db, "Writer").ID
|
||||
uaPersistedUser(t, db, 5, uaRoleIDPtr(admin), true)
|
||||
db.Exec("UPDATE users SET role_assigned_by_id = 3 WHERE id = 5")
|
||||
if _, err := persistence.NewRoleRepository(db).ReassignMembers(admin, writer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if row := userRow(t, db, 5); roleIDOf(row) != writer || row.RoleAssignedByID == nil || *row.RoleAssignedByID != 3 {
|
||||
t.Errorf("unexpected %+v", row)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAssignedByAcceptFailsOnInviterLookupError(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
w.users.fail.failNextWith("Get", errors.New("inviter lookup failed"))
|
||||
before := len(w.users.users)
|
||||
rec := w.anonymous(t, http.MethodPost, "/v1/auth/invite/accept", `{"code": "code3", "username": "kim", "password": "password1"}`)
|
||||
if rec.Code < 500 {
|
||||
t.Fatalf("expected a failure, got %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
if len(w.users.users) != before {
|
||||
t.Errorf("no user may be created")
|
||||
}
|
||||
if w.invites.invites[3].AcceptedAt != nil {
|
||||
t.Errorf("the invite must stay unaccepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleAssignedByChangeRoleAuditOfDanglingRole(t *testing.T) {
|
||||
w := newUsersAPIWorld(t)
|
||||
member := w.users.users[41]
|
||||
member.RoleID = 9999
|
||||
w.users.users[41] = member
|
||||
rec := w.putRole(t, uaAdmin, "41", w.idText("Writer"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
event := w.recorder.events[len(w.recorder.events)-1]
|
||||
if _, present := event.Params[audit.ParamFrom]; present {
|
||||
t.Errorf("a role that was not held leaves from absent, got %v", event.Params)
|
||||
}
|
||||
if event.Params[audit.ParamTo] != w.ids["Writer"] {
|
||||
t.Errorf("to = %v", event.Params[audit.ParamTo])
|
||||
}
|
||||
if len(event.Changes) != 0 {
|
||||
t.Errorf("no blank role change expected, got %v", event.Changes)
|
||||
}
|
||||
}
|
||||
@@ -864,7 +864,7 @@ func TestContentTypeTypedGrantCleanup(t *testing.T) {
|
||||
func TestGetMyAccessTypedGrants(t *testing.T) {
|
||||
t.Run("the events editor lists typed entries including none, in order", func(t *testing.T) {
|
||||
world := newRolesAPIWorld(t)
|
||||
access, err := useCases.NewGetMyAccessUseCase(world.roles).Get(world.users.users[raEventsEditor])
|
||||
access, err := useCases.NewGetMyAccessUseCase(world.roles, world.users).Get(world.users.users[raEventsEditor])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -878,7 +878,7 @@ func TestGetMyAccessTypedGrants(t *testing.T) {
|
||||
world := newRolesAPIWorld(t)
|
||||
hand, _ := world.roles.Create(role.Role{Name: "Hand placed", Version: 1, Grants: grantRows("system.apply_updates=all", "content.read=all", "content.read=own")})
|
||||
world.addUser(60, hand.ID, true)
|
||||
access, _ := useCases.NewGetMyAccessUseCase(world.roles).Get(world.users.users[60])
|
||||
access, _ := useCases.NewGetMyAccessUseCase(world.roles, world.users).Get(world.users.users[60])
|
||||
if !reflect.DeepEqual(access.Permissions, grantRows("content.read=own")) {
|
||||
t.Fatalf("unexpected permissions %v", access.Permissions)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestContentScopeGetMyAccessUseCase(t *testing.T) {
|
||||
roles := newSeededMockRoleRepo()
|
||||
counting := &contentScopeRoleRepo{Repository: roles}
|
||||
custom, _ := roles.Create(role.Role{Name: "Messy", Version: 1, Grants: grantRows("content.update=own", "content.update=all", "settings.delete_site=all", "content.update:blog=all", "schema.view=own")})
|
||||
access, err := useCases.NewGetMyAccessUseCase(counting).Get(user.User{ID: 30, RoleID: custom.ID})
|
||||
access, err := useCases.NewGetMyAccessUseCase(counting, newMockUserRepo()).Get(user.User{ID: 30, RoleID: custom.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func TestContentScopeGetMyAccessUseCase(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("The Owner holds the 34 keys", func(t *testing.T) {
|
||||
access, err := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()).Get(user.User{ID: 3, RoleID: ownerRoleID})
|
||||
access, err := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()).Get(user.User{ID: 3, RoleID: ownerRoleID})
|
||||
if err != nil || len(access.Permissions) != 34 {
|
||||
t.Errorf("expected 34 permissions, got %d (%v)", len(access.Permissions), err)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func (w *contentScopeWorld) wire() {
|
||||
c := w.container
|
||||
c.GetUserUseCase = useCases.NewGetUserUseCase(w.userRepo)
|
||||
c.CheckPermissionUseCase = useCases.NewCheckPermissionUseCase(w.roleRepo)
|
||||
c.GetMyAccessUseCase = useCases.NewGetMyAccessUseCase(w.roleRepo)
|
||||
c.GetMyAccessUseCase = useCases.NewGetMyAccessUseCase(w.roleRepo, w.userRepo)
|
||||
c.AuthenticateAPIKeyUseCase = useCases.NewAuthenticateAPIKeyUseCase(w.keys, w.generator, w.userRepo, clock)
|
||||
c.ResolveArticleSEOUseCase = useCases.NewResolveArticleSEOUseCase(w.articles, plainSites(), w.images)
|
||||
c.CheckArticleSEOUseCase = useCases.NewCheckArticleSEOUseCase(w.articles, plainSites(), w.images)
|
||||
|
||||
@@ -352,14 +352,14 @@ func TestNewArticlesRecordTheirCreator(t *testing.T) {
|
||||
|
||||
func TestGetMyAccessUseCase(t *testing.T) {
|
||||
t.Run("A user with role_id 0 gets an error and is never promoted", func(t *testing.T) {
|
||||
uc := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo())
|
||||
uc := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo())
|
||||
if _, err := uc.Get(user.User{ID: 11}); !errors.Is(err, role.ErrRoleNotFound) {
|
||||
t.Errorf("expected ErrRoleNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A user whose role cannot be resolved gets an error", func(t *testing.T) {
|
||||
uc := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo())
|
||||
uc := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo())
|
||||
if _, err := uc.Get(user.User{ID: 13, RoleID: 99}); !errors.Is(err, role.ErrRoleNotFound) {
|
||||
t.Errorf("expected ErrRoleNotFound, got %v", err)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ func newRuntimeSettingsContainer(t *testing.T) *api.UseCases {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(userRepo),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, newSeededMockRoleRepo(), hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
|
||||
@@ -177,7 +177,7 @@ func newSessionRevocationEnv(t *testing.T) *sessionRevocationEnv {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(userRepo),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, newSeededMockRoleRepo(), hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
||||
}
|
||||
|
||||
@@ -192,6 +192,11 @@ func TestSQLiteSchemaIsUntouchedByTheModelChanges(t *testing.T) {
|
||||
}
|
||||
for _, row := range after.rows["users"] {
|
||||
delete(row, "role_id")
|
||||
assignedBy, columnAdded := row["role_assigned_by_id"]
|
||||
if !columnAdded || assignedBy != nil {
|
||||
t.Errorf("a legacy user must read a NULL role_assigned_by_id: %v", row)
|
||||
}
|
||||
delete(row, "role_assigned_by_id")
|
||||
}
|
||||
for _, row := range after.rows["invites"] {
|
||||
if row["role_id"] == nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func newDownloadHTTP(t *testing.T, rig *downloadRig) *updateHarness {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(users),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
GetUpdateStatusUseCase: rig.status,
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now, discardRecorder),
|
||||
StartUpdateDownloadUseCase: rig.start,
|
||||
|
||||
@@ -65,7 +65,7 @@ func newUpdateHarness(t *testing.T, running string) *updateHarness {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(users),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
GetUpdateStatusUseCase: newUpdateStatusUseCase(store, &fakeJobStore{}, &fakeHelperDetector{}, running),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, store, running, now, discardRecorder),
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func newRestartHTTP(t *testing.T) *restartHTTP {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(newUpdateUserRepo()),
|
||||
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()),
|
||||
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()),
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(
|
||||
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, false, h.clock.Now),
|
||||
RequestRestartUseCase: useCases.NewRequestRestartUseCase(
|
||||
|
||||
@@ -27,7 +27,7 @@ func newDisplayNameRouter(userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mu
|
||||
getUser := useCases.NewGetUserUseCase(userRepo)
|
||||
update := useCases.NewUpdateUserProfileUseCase(userRepo, discardRecorder)
|
||||
handler := userhandler.NewHandler(update)
|
||||
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()))
|
||||
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()))
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, newSeededMockRoleRepo(), hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
|
||||
@@ -56,7 +56,7 @@ func newUserRolesFixture(t *testing.T, maxAttempts int) *userRolesFixture {
|
||||
f.token,
|
||||
true,
|
||||
)
|
||||
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo()))
|
||||
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()), useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()))
|
||||
inviteHandler := userhandler.NewInviteHandler(
|
||||
useCases.NewCreateInviteUseCase(f.users, f.invites, newSeededMockRoleRepo(), newMockContentTypeRepo(), hasher, f.mail, invitePublicURL, discardRecorder),
|
||||
nil,
|
||||
@@ -250,7 +250,7 @@ func TestUserRolesHttp(t *testing.T) {
|
||||
assertExactKeys(t, item, "id", "kind", "email", "role", "status", "created_at")
|
||||
continue
|
||||
}
|
||||
assertExactKeys(t, item, "id", "kind", "username", "display_name", "email", "role", "status", "created_at")
|
||||
assertExactKeys(t, item, "id", "kind", "username", "display_name", "email", "role", "role_assigned_by", "status", "created_at")
|
||||
want := wantRoles[decodedString(t, item, "username")]
|
||||
var ref struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
+6
-1
@@ -49,6 +49,7 @@ type mockUserRepo struct {
|
||||
updateDisplayNameCalls int
|
||||
lastUpdateDisplayName string
|
||||
getAllCalls int
|
||||
getCalls int
|
||||
updateRoleCalls int
|
||||
transferCalls int
|
||||
beforeUpdateRole func()
|
||||
@@ -77,6 +78,7 @@ func (m *mockUserRepo) sortedIds() []uint32 {
|
||||
}
|
||||
|
||||
func (m *mockUserRepo) Get(id uint32) (user.User, error) {
|
||||
m.getCalls++
|
||||
if err := m.fail.trigger("Get"); err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
@@ -271,7 +273,7 @@ func (m *mockUserRepo) roleExists(id uint32) bool {
|
||||
return present
|
||||
}
|
||||
|
||||
func (m *mockUserRepo) UpdateRole(id uint32, from uint32, to uint32) (user.User, error) {
|
||||
func (m *mockUserRepo) UpdateRole(id uint32, from uint32, to uint32, assignedBy uint32) (user.User, error) {
|
||||
m.updateRoleCalls++
|
||||
if err := m.fail.trigger("UpdateRole"); err != nil {
|
||||
return user.User{}, err
|
||||
@@ -292,6 +294,7 @@ func (m *mockUserRepo) UpdateRole(id uint32, from uint32, to uint32) (user.User,
|
||||
return user.User{}, user.ErrRoleChanged
|
||||
}
|
||||
stored.RoleID = to
|
||||
stored.RoleAssignedByID = assignedBy
|
||||
m.users[id] = stored
|
||||
return stored, nil
|
||||
}
|
||||
@@ -315,7 +318,9 @@ func (m *mockUserRepo) TransferOwnership(from uint32, to uint32, ownerRoleID uin
|
||||
return user.ErrInvalidTransfer
|
||||
}
|
||||
former.RoleID = formerRoleID
|
||||
former.RoleAssignedByID = 0
|
||||
successor.RoleID = ownerRoleID
|
||||
successor.RoleAssignedByID = from
|
||||
m.users[from] = former
|
||||
m.users[to] = successor
|
||||
return nil
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestUsersAPIChangeRoleHTTP(t *testing.T) {
|
||||
if rec.Code != 200 || item["kind"] != "user" || item["status"] != "accepted" {
|
||||
t.Fatalf("unexpected %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "username", "display_name", "email", "role", "status", "created_at")
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "username", "display_name", "email", "role", "role_assigned_by", "status", "created_at")
|
||||
if ref := usersAPIRoleRef(t, item); ref["name"] != row.role || ref["id"] != float64(w.ids[row.role]) {
|
||||
t.Errorf("unexpected role %v", ref)
|
||||
}
|
||||
@@ -341,6 +341,9 @@ func TestUsersAPIInviteHTTP(t *testing.T) {
|
||||
default:
|
||||
assertRefusal(t, rec, row.status, row.code)
|
||||
}
|
||||
if got := rec.Header().Get(apperr.ErrorCodeHeader); got != inviteConflictCodes[row.code] {
|
||||
t.Errorf("%s = %q, expected %q", apperr.ErrorCodeHeader, got, inviteConflictCodes[row.code])
|
||||
}
|
||||
if len(w.mail.SentEmails) != 0 || len(w.invites.invites) != 4 {
|
||||
t.Errorf("a refusal creates no invite and sends no mail")
|
||||
}
|
||||
@@ -565,7 +568,7 @@ func TestUsersAPIListHTTP(t *testing.T) {
|
||||
seen[key] = true
|
||||
switch kind {
|
||||
case "user":
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "username", "display_name", "email", "role", "status", "created_at")
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "username", "display_name", "email", "role", "role_assigned_by", "status", "created_at")
|
||||
default:
|
||||
apiKeyAssertExactFields(t, item, "id", "kind", "email", "role", "status", "created_at")
|
||||
}
|
||||
@@ -636,3 +639,8 @@ func TestUsersAPIPublicUserShape(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var inviteConflictCodes = map[string]string{
|
||||
apperr.PendingInviteErrorMessage: "invite_pending",
|
||||
apperr.EmailHasAccountErrorMessage: "email_has_account",
|
||||
}
|
||||
@@ -62,20 +62,20 @@ func TestUsersAPIUserRepository(t *testing.T) {
|
||||
repo := persistence.NewUserRepository(db)
|
||||
admin, writer, owner := storedRole(t, db, "Admin").ID, storedRole(t, db, "Writer").ID, storedRole(t, db, "Owner").ID
|
||||
uaPersistedUser(t, db, 5, &admin, true)
|
||||
updated, err := repo.UpdateRole(5, admin, writer)
|
||||
updated, err := repo.UpdateRole(5, admin, writer, 0)
|
||||
if err != nil || updated.RoleID != writer || updated.TokenVersion != 0 {
|
||||
t.Fatalf("unexpected %+v, %v", updated, err)
|
||||
}
|
||||
if _, err := repo.UpdateRole(5, admin, owner); !errors.Is(err, user.ErrRoleChanged) {
|
||||
if _, err := repo.UpdateRole(5, admin, owner, 0); !errors.Is(err, user.ErrRoleChanged) {
|
||||
t.Fatalf("the second call with the same from must lose, got %v", err)
|
||||
}
|
||||
if roleIDOf(userRow(t, db, 5)) != writer {
|
||||
t.Errorf("the winner must stand")
|
||||
}
|
||||
if _, err := repo.UpdateRole(999, admin, writer); !errors.Is(err, user.ErrUserNotFound) {
|
||||
if _, err := repo.UpdateRole(999, admin, writer, 0); !errors.Is(err, user.ErrUserNotFound) {
|
||||
t.Errorf("expected ErrUserNotFound, got %v", err)
|
||||
}
|
||||
if _, err := repo.UpdateRole(5, writer, 999); !errors.Is(err, role.ErrRoleNotFound) {
|
||||
if _, err := repo.UpdateRole(5, writer, 999, 0); !errors.Is(err, role.ErrRoleNotFound) {
|
||||
t.Errorf("expected ErrRoleNotFound, got %v", err)
|
||||
}
|
||||
if roleIDOf(userRow(t, db, 5)) != writer {
|
||||
@@ -88,7 +88,7 @@ func TestUsersAPIUserRepository(t *testing.T) {
|
||||
repo := persistence.NewUserRepository(db)
|
||||
writer := storedRole(t, db, "Writer").ID
|
||||
uaPersistedUser(t, db, 5, nil, true)
|
||||
if _, err := repo.UpdateRole(5, 0, writer); err != nil || roleIDOf(userRow(t, db, 5)) != writer {
|
||||
if _, err := repo.UpdateRole(5, 0, writer, 0); err != nil || roleIDOf(userRow(t, db, 5)) != writer {
|
||||
t.Fatalf("unexpected %v", err)
|
||||
}
|
||||
})
|
||||
@@ -98,7 +98,7 @@ func TestUsersAPIUserRepository(t *testing.T) {
|
||||
repo := persistence.NewUserRepository(db)
|
||||
sqlDB, _ := db.DB()
|
||||
_ = sqlDB.Close()
|
||||
if _, err := repo.UpdateRole(5, 1, 2); err == nil {
|
||||
if _, err := repo.UpdateRole(5, 1, 2, 0); err == nil {
|
||||
t.Errorf("expected an error")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -113,6 +113,7 @@ func (w *usersAPIWorld) wire() {
|
||||
hasher := &stubHasher{}
|
||||
url := "https://cms.example.com"
|
||||
c.ChangeUserRoleUseCase = useCases.NewChangeUserRoleUseCase(w.users, w.roles, w.types, w.recorder)
|
||||
c.GetMyAccessUseCase = useCases.NewGetMyAccessUseCase(w.roles, w.users)
|
||||
c.TransferOwnershipUseCase = useCases.NewTransferOwnershipUseCase(w.users, w.roles, w.recorder)
|
||||
c.CreateInviteUseCase = useCases.NewCreateInviteUseCase(w.users, w.invites, w.roles, w.types, hasher, w.mail, url, w.recorder)
|
||||
c.RenewInviteUseCase = useCases.NewRenewInviteUseCase(w.users, w.invites, w.roles, w.types, hasher, w.mail, url, w.recorder)
|
||||
|
||||
Reference in new issue
Block a user