package test import ( "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/invite" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "time" ) const ( uaOwner uint32 = 3 uaAdmin uint32 = 5 uaWriter uint32 = 9 uaOtherAdmin uint32 = 10 uaUnverified uint32 = 12 uaNoRole uint32 = 13 uaGhostRole uint32 = 14 uaPeople uint32 = 31 uaViewer uint32 = 32 uaInviter uint32 = 33 uaContributor uint32 = 41 uaContentLead uint32 = 45 uaIntern uint32 = 46 uaUnknownRole uint32 = 999 ) type usersAPIRoleSpec struct { name string grants []string members []uint32 } var usersAPICustomRoles = []usersAPIRoleSpec{ {"Contributor", []string{"content.read=all", "content.create=all", "content.update=own"}, []uint32{41, 42, 43, 44}}, {"Intern", []string{"content.read=own"}, []uint32{uaIntern}}, {"Content lead", []string{"content.read=all", "content.update=all", "content.publish=all", "content.delete=all"}, []uint32{uaContentLead}}, {"People manager", []string{"users.view=all", "users.invite=all", "users.change_role=all", "content.read=all", "content.create=all", "content.update=own", "content.publish=own", "schema.view=all"}, []uint32{uaPeople}}, {"Viewer", []string{"users.view=all", "roles.view=all"}, []uint32{uaViewer}}, {"Inviter", []string{"users.invite=all"}, []uint32{uaInviter}}, } type usersAPIWorld struct { *apiKeyEnv recorder *contentScopeRecorder types *mockContentTypeRepo ids map[string]uint32 } func newUsersAPIWorld(t *testing.T) *usersAPIWorld { t.Helper() recorder := &contentScopeRecorder{} env := newAPIKeyEnvRecording(t, recorder) world := &usersAPIWorld{apiKeyEnv: env, recorder: recorder, types: newMockContentTypeRepo(), ids: map[string]uint32{}} env.users.users = map[uint32]user.User{} env.users.nextID = 100 env.users.knownRoles = env.roles env.roles.users = env.users env.roles.invites = env.invites world.seedUsers() world.seedInvites() world.seedContentTypes() world.wire() return world } func (w *usersAPIWorld) addMember(id uint32, username string, email string, roleID uint32, verified bool) { w.users.users[id] = user.User{ID: id, Username: username, Email: email, IsVerified: verified, RoleID: roleID, Password: "hashed:password1"} } func (w *usersAPIWorld) seedUsers() { w.ids["Owner"], w.ids["Admin"], w.ids["Writer"] = ownerRoleID, adminRoleID, writerRoleID w.addMember(uaOwner, "marta", "marta@renew.example", ownerRoleID, true) w.addMember(uaAdmin, "jonas", "jonas@renew.example", adminRoleID, true) w.addMember(uaOtherAdmin, "lea", "lea@renew.example", adminRoleID, true) w.addMember(uaUnverified, "sam", "sam@renew.example", adminRoleID, false) w.addMember(uaWriter, "ines", "ines@renew.example", writerRoleID, true) w.addMember(uaNoRole, "zero", "zero@renew.example", 0, true) w.addMember(uaGhostRole, "ghost", "ghost@renew.example", 99, true) for _, spec := range usersAPICustomRoles { created, _ := w.roles.Create(role.Role{Name: spec.name, Version: 1, Grants: grantRows(spec.grants...)}) w.ids[spec.name] = created.ID for _, member := range spec.members { w.addMember(member, "user"+strconv.Itoa(int(member)), "user"+strconv.Itoa(int(member))+"@renew.example", created.ID, true) } } } func (w *usersAPIWorld) seedInvites() { now := time.Now() accepted := now.Add(-24 * time.Hour) w.invites.seed(invite.Invite{Email: "new1@renew.example", HashedCode: "hashed:code1", InvitedBy: uaAdmin, RoleID: writerRoleID, CreatedAt: now.Add(-time.Hour), ExpiresAt: now.Add(48 * time.Hour)}) w.invites.seed(invite.Invite{Email: "old2@renew.example", HashedCode: "hashed:code2", InvitedBy: uaAdmin, RoleID: w.ids["Contributor"], CreatedAt: now.Add(-73 * time.Hour), ExpiresAt: now.Add(-time.Hour)}) w.invites.seed(invite.Invite{Email: "new3@renew.example", HashedCode: "hashed:code3", InvitedBy: uaAdmin, RoleID: w.ids["Contributor"], CreatedAt: now.Add(-2 * time.Hour), ExpiresAt: now.Add(48 * time.Hour)}) w.invites.seed(invite.Invite{Email: "ines@renew.example", HashedCode: "hashed:code4", InvitedBy: uaAdmin, RoleID: writerRoleID, CreatedAt: now.Add(-48 * time.Hour), ExpiresAt: now.Add(24 * time.Hour), AcceptedAt: &accepted}) } func (w *usersAPIWorld) seedContentTypes() { title := contenttype.Field{Key: "title", Label: "Title", Type: contenttype.TypeText, Required: true, Options: []string{}} w.types.types[1] = contenttype.ContentType{ID: 1, Key: "events", Name: "Events", Fields: []contenttype.Field{title}} w.types.types[2] = contenttype.ContentType{ID: 2, Key: "venues", Name: "Venues", Fields: []contenttype.Field{title}} w.types.nextID = 10 } func (w *usersAPIWorld) wire() { c := w.container 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) c.CancelInviteUseCase = useCases.NewCancelInviteUseCase(w.invites, w.recorder) c.ListUsersAndInvitesUseCase = useCases.NewListUsersAndInvitesUseCase(w.users, w.invites, w.roles) c.AcceptInviteUseCase = useCases.NewAcceptInviteUseCase(w.users, w.invites, w.roles, hasher, w.recorder) c.CreateUserUseCase = useCases.NewCreateUserUseCase(w.users, w.roles, hasher, w.mail, url, w.recorder) c.ListRolesUseCase = useCases.NewListRolesUseCase(w.roles, w.users, w.types) c.GetRoleUseCase = useCases.NewGetRoleUseCase(w.roles, w.users, w.types) c.CreateRoleUseCase = useCases.NewCreateRoleUseCase(w.roles, w.users, w.types, w.recorder) c.DeleteRoleUseCase = useCases.NewDeleteRoleUseCase(w.roles, w.users, w.types, w.recorder) c.ReassignRoleMembersUseCase = useCases.NewReassignRoleMembersUseCase(w.roles, w.users, w.types, w.recorder) w.rebuildRouter() } func (w *usersAPIWorld) id(name string) uint32 { return w.ids[name] } func (w *usersAPIWorld) idText(name string) string { return strconv.FormatUint(uint64(w.ids[name]), 10) } func (w *usersAPIWorld) caller(userID uint32) useCases.ContentCaller { return useCases.ContentCaller{UserID: userID, Grants: w.roles.roles[w.users.users[userID].RoleID].GrantSet()} } func (w *usersAPIWorld) do(t *testing.T, caller uint32, method string, path string, body string) *httptest.ResponseRecorder { t.Helper() return w.send(t, apiKeyRequest{method: method, path: path, body: body, cookieUser: caller}) } func (w *usersAPIWorld) putRole(t *testing.T, caller uint32, target string, roleID string) *httptest.ResponseRecorder { t.Helper() return w.do(t, caller, http.MethodPut, "/v1/user/"+target+"/role", `{"role_id": `+roleID+`}`) } func (w *usersAPIWorld) storedRoleID(userID uint32) uint32 { return w.users.users[userID].RoleID } func (w *usersAPIWorld) createTemp(grants ...string) uint32 { created, _ := w.roles.Create(role.Role{Name: "Temp", Version: 1, Grants: grantRows(grants...)}) return created.ID } func usersAPIDecode(t *testing.T, rec *httptest.ResponseRecorder) map[string]any { t.Helper() var object map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &object); err != nil { t.Fatalf("expected a JSON object, got %q: %v", rec.Body.String(), err) } return object } func usersAPIRoleRef(t *testing.T, object map[string]any) map[string]any { t.Helper() ref, ok := object["role"].(map[string]any) if !ok { t.Fatalf("expected role to be an object, got %v", object["role"]) } apiKeyAssertExactFields(t, ref, "id", "name", "built_in", "locked") return ref } func usersAPIEventActions(w *usersAPIWorld) []string { return w.recorder.actions() }