graph:Add stubs for education/classes endpoints (#5360)

* Renamed files for consistency reasons

err_school.go implements the full education interface not just schools.
ldap_school.go renamed to ldap_education_school.go for making it
consistent with ldap_education_user.go

* graph: Add stubs for education/classes endpoints

The acutal backend implementations are still empty.
This commit is contained in:
Ralf Haferkamp
2023-01-11 20:38:18 +01:00
committed by GitHub
parent 92db62698d
commit 709ef1ffca
13 changed files with 1277 additions and 10 deletions

View File

@@ -60,6 +60,17 @@ type EducationBackend interface {
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error
// GetEducationClasses lists all classes
GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error)
// GetEducationClasses reads a given class by id
GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error)
// CreateEducationClass creates the supplied education class in the identity backend.
CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error)
// DeleteEducationClass deletes the supplied education class in the identity backend.
DeleteEducationClass(ctx context.Context, nameOrID string) error
// GetEducationClassMembers returns the EducationUser members for an EducationClass
GetEducationClassMembers(ctx context.Context, nameOrID string) ([]*libregraph.EducationUser, error)
// CreateEducationUser creates a given education user in the identity backend.
CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error)
// DeleteEducationUser deletes a given educationuser, identified by username or id, from the backend

View File

@@ -50,6 +50,31 @@ func (i *ErrEducationBackend) RemoveUserFromEducationSchool(ctx context.Context,
return errNotImplemented
}
// GetEducationClasses implements the EducationBackend interface
func (i *ErrEducationBackend) GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// GetEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// CreateEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// DeleteEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) DeleteEducationClass(ctx context.Context, nameOrID string) error {
return errNotImplemented
}
// GetEducationClassMembers implements the EducationBackend interface
func (i *ErrEducationBackend) GetEducationClassMembers(ctx context.Context, nameOrID string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// CreateEducationUser creates a given education user in the identity backend.
func (i *ErrEducationBackend) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
return nil, errNotImplemented

View File

@@ -0,0 +1,33 @@
package identity
import (
"context"
"net/url"
libregraph "github.com/owncloud/libre-graph-api-go"
)
// GetEducationClasses implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// CreateEducationClass implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// GetEducationClass implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// DeleteEducationClass implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) DeleteEducationClass(ctx context.Context, nameOrID string) error {
return errNotImplemented
}
// GetEducationClassMembers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationClassMembers(ctx context.Context, nameOrID string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}

View File

@@ -31,6 +31,29 @@ func (_m *EducationBackend) AddUsersToEducationSchool(ctx context.Context, schoo
return r0
}
// CreateEducationClass provides a mock function with given fields: ctx, class
func (_m *EducationBackend) CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
ret := _m.Called(ctx, class)
var r0 *libregraph.EducationClass
if rf, ok := ret.Get(0).(func(context.Context, libregraph.EducationClass) *libregraph.EducationClass); ok {
r0 = rf(ctx, class)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationClass)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, libregraph.EducationClass) error); ok {
r1 = rf(ctx, class)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateEducationSchool provides a mock function with given fields: ctx, group
func (_m *EducationBackend) CreateEducationSchool(ctx context.Context, group libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
ret := _m.Called(ctx, group)
@@ -77,6 +100,20 @@ func (_m *EducationBackend) CreateEducationUser(ctx context.Context, user libreg
return r0, r1
}
// DeleteEducationClass provides a mock function with given fields: ctx, nameOrID
func (_m *EducationBackend) DeleteEducationClass(ctx context.Context, nameOrID string) error {
ret := _m.Called(ctx, nameOrID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, nameOrID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteEducationSchool provides a mock function with given fields: ctx, id
func (_m *EducationBackend) DeleteEducationSchool(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
@@ -105,6 +142,75 @@ func (_m *EducationBackend) DeleteEducationUser(ctx context.Context, nameOrID st
return r0
}
// GetEducationClass provides a mock function with given fields: ctx, namedOrID, queryParam
func (_m *EducationBackend) GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error) {
ret := _m.Called(ctx, namedOrID, queryParam)
var r0 *libregraph.EducationClass
if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.EducationClass); ok {
r0 = rf(ctx, namedOrID, queryParam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationClass)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
r1 = rf(ctx, namedOrID, queryParam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEducationClassMembers provides a mock function with given fields: ctx, nameOrID
func (_m *EducationBackend) GetEducationClassMembers(ctx context.Context, nameOrID string) ([]*libregraph.EducationUser, error) {
ret := _m.Called(ctx, nameOrID)
var r0 []*libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.EducationUser); ok {
r0 = rf(ctx, nameOrID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, nameOrID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEducationClasses provides a mock function with given fields: ctx, queryParam
func (_m *EducationBackend) GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) {
ret := _m.Called(ctx, queryParam)
var r0 []*libregraph.EducationClass
if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.EducationClass); ok {
r0 = rf(ctx, queryParam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationClass)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
r1 = rf(ctx, queryParam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEducationSchool provides a mock function with given fields: ctx, nameOrID, queryParam
func (_m *EducationBackend) GetEducationSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error) {
ret := _m.Called(ctx, nameOrID, queryParam)

View File

@@ -0,0 +1,433 @@
package svc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"github.com/CiscoM31/godata"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// GetEducationClasses implements the Service interface.
func (g Graph) GetEducationClasses(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("query", r.URL.Query()).Msg("calling GetEducationClasses")
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg(
"could not get educationClasses: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
classes, err := g.identityEducationBackend.GetEducationClasses(r.Context(), r.URL.Query())
if err != nil {
logger.Debug().Err(err).Msg("could not get classes: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
classes, err = sortClasses(odataReq, classes)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("cannot get classes: could not sort classes according to query")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: classes})
}
// PostEducationClass implements the Service interface.
func (g Graph) PostEducationClass(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling post EducationClass")
class := libregraph.NewEducationClassWithDefaults()
err := json.NewDecoder(r.Body).Decode(class)
if err != nil {
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not create education class: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
if _, ok := class.GetDisplayNameOk(); !ok {
logger.Debug().Err(err).Interface("class", class).Msg("could not create class: missing required attribute")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute")
return
}
// Disallow user-supplied IDs. It's supposed to be readonly. We're either
// generating them in the backend ourselves or rely on the Backend's
// storage (e.g. LDAP) to provide a unique ID.
if _, ok := class.GetIdOk(); ok {
logger.Debug().Msg("could not create class: id is a read-only attribute")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "class id is a read-only attribute")
return
}
if class, err = g.identityEducationBackend.CreateEducationClass(r.Context(), *class); err != nil {
logger.Debug().Interface("class", class).Msg("could not create class: backend error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
/* TODO requires reva changes
if class != nil && class.Id != nil {
currentUser := revactx.ContextMustGetUser(r.Context())
g.publishEvent(events.EducationClassCreated{Executant: currentUser.Id, EducationClassID: *class.Id})
}
*/
render.Status(r, http.StatusCreated)
render.JSON(w, r, class)
}
// PatchEducationClass implements the Service interface.
func (g Graph) PatchEducationClass(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling patch education class")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Str("id", classID).Msg("could not change class: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not change class: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
changes := libregraph.NewEducationClassWithDefaults()
err = json.NewDecoder(r.Body).Decode(changes)
if err != nil {
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not change class: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
if memberRefs, ok := changes.GetMembersodataBindOk(); ok {
// The spec defines a limit of 20 members maxium per Request
if len(memberRefs) > g.config.API.GroupMembersPatchLimit {
logger.Debug().
Int("number", len(memberRefs)).
Int("limit", g.config.API.GroupMembersPatchLimit).
Msg("could not add group members, exceeded members limit")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest,
fmt.Sprintf("Request is limited to %d members", g.config.API.GroupMembersPatchLimit))
return
}
memberIDs := make([]string, 0, len(memberRefs))
for _, memberRef := range memberRefs {
memberType, id, err := g.parseMemberRef(memberRef)
if err != nil {
logger.Debug().
Str("memberref", memberRef).
Msg("could not change class: Error parsing member@odata.bind values")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Error parsing member@odata.bind values")
return
}
logger.Debug().Str("membertype", memberType).Str("memberid", id).Msg("add class member")
// The MS Graph spec allows "directoryObject", "user", "class" and "organizational Contact"
// we restrict this to users for now. Might add Classes as members later
if memberType != memberTypeUsers {
logger.Debug().
Str("type", memberType).
Msg("could not change class: could not add member, only user type is allowed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only user are allowed as class members")
return
}
memberIDs = append(memberIDs, id)
}
err = g.identityBackend.AddMembersToGroup(r.Context(), classID, memberIDs)
}
if err != nil {
logger.Debug().Err(err).Msg("could not change class: backend could not add members")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
render.Status(r, http.StatusNoContent) // TODO StatusNoContent when prefer=minimal is used, otherwise OK and the resource in the body
render.NoContent(w, r)
}
// GetEducationClass implements the Service interface.
func (g Graph) GetEducationClass(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get education class")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Str("id", classID).Msg("could not get class: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
}
if classID == "" {
logger.Debug().Msg("could not get class: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
logger.Debug().
Str("id", classID).
Interface("query", r.URL.Query()).
Msg("calling get class on backend")
class, err := g.identityEducationBackend.GetEducationClass(r.Context(), classID, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, class)
}
// DeleteEducationClass implements the Service interface.
func (g Graph) DeleteEducationClass(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete class")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Err(err).Str("id", classID).Msg("could not delete class: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not delete class: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
logger.Debug().Str("id", classID).Msg("calling delete class on backend")
err = g.identityEducationBackend.DeleteEducationClass(r.Context(), classID)
if err != nil {
logger.Debug().Err(err).Msg("could not delete class: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
/* TODO requires reva changes
currentUser := revactx.ContextMustGetUser(r.Context())
g.publishEvent(events.ClassDeleted{Executant: currentUser.Id, ClassID: classID})
*/
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// GetEducationClassMembers implements the Service interface.
func (g Graph) GetEducationClassMembers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get class members")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Str("id", classID).Msg("could not get class members: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not get class members: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
logger.Debug().Str("id", classID).Msg("calling get class members on backend")
members, err := g.identityEducationBackend.GetEducationClassMembers(r.Context(), classID)
if err != nil {
logger.Debug().Err(err).Msg("could not get class members: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, members)
}
// PostEducationClassMember implements the Service interface.
func (g Graph) PostEducationClassMember(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("Calling post class member")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().
Err(err).
Str("id", classID).
Msg("could not add member to class: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not add class member: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
memberRef := libregraph.NewMemberReference()
err = json.NewDecoder(r.Body).Decode(memberRef)
if err != nil {
logger.Debug().
Err(err).
Interface("body", r.Body).
Msg("could not add class member: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
memberRefURL, ok := memberRef.GetOdataIdOk()
if !ok {
logger.Debug().Msg("could not add class member: @odata.id reference is missing")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "@odata.id reference is missing")
return
}
memberType, id, err := g.parseMemberRef(*memberRefURL)
if err != nil {
logger.Debug().Err(err).Msg("could not add class member: error parsing @odata.id url")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Error parsing @odata.id url")
return
}
// The MS Graph spec allows "directoryObject", "user", "class" and "organizational Contact"
// we restrict this to users for now. Might add EducationClass as members later
if memberType != memberTypeUsers {
logger.Debug().Str("type", memberType).Msg("could not add class member: Only users are allowed as class members")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as class members")
return
}
logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add member on backend")
err = g.identityBackend.AddMembersToGroup(r.Context(), classID, []string{id})
if err != nil {
logger.Debug().Err(err).Msg("could not add class member: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
/* TODO requires reva changes
currentUser := revactx.ContextMustGetUser(r.Context())
g.publishEvent(events.EducationClassMemberAdded{Executant: currentUser.Id, EducationClassID: classID, UserID: id})
*/
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// DeleteEducationClassMember implements the Service interface.
func (g Graph) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete class member")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Err(err).Str("id", classID).Msg("could not delete class member: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not delete class member: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
memberID := chi.URLParam(r, "memberID")
memberID, err = url.PathUnescape(memberID)
if err != nil {
logger.Debug().Err(err).Str("id", memberID).Msg("could not delete class member: unescaping member id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping member id failed")
return
}
if memberID == "" {
logger.Debug().Msg("could not delete class member: missing member id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing member id")
return
}
logger.Debug().Str("classID", classID).Str("memberID", memberID).Msg("calling delete member on backend")
err = g.identityBackend.RemoveMemberFromGroup(r.Context(), classID, memberID)
if err != nil {
logger.Debug().Err(err).Msg("could not delete class member: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
/* TODO requires reva changes
currentUser := revactx.ContextMustGetUser(r.Context())
g.publishEvent(events.EducationClassMemberRemoved{Executant: currentUser.Id, EducationClassID: classID, UserID: memberID})
*/
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
func sortClasses(req *godata.GoDataRequest, classes []*libregraph.EducationClass) ([]*libregraph.EducationClass, error) {
if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 {
return classes, nil
}
var less func(i, j int) bool
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
case displayNameAttr:
less = func(i, j int) bool {
return strings.ToLower(classes[i].GetDisplayName()) < strings.ToLower(classes[j].GetDisplayName())
}
default:
return nil, fmt.Errorf("we do not support <%s> as a order parameter", req.Query.OrderBy.OrderByItems[0].Field.Value)
}
if req.Query.OrderBy.OrderByItems[0].Order == _sortDescending {
sort.Slice(classes, reverse(less))
} else {
sort.Slice(classes, less)
}
return classes, nil
}

View File

@@ -0,0 +1,530 @@
package svc_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/test-go/testify/mock"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
type classList struct {
Value []*libregraph.EducationClass
}
var _ = Describe("EducationClass", func() {
var (
svc service.Service
ctx context.Context
cfg *config.Config
gatewayClient *mocks.GatewayClient
eventsPublisher mocks.Publisher
identityBackend *identitymocks.Backend
identityEducationBackend *identitymocks.EducationBackend
rr *httptest.ResponseRecorder
newClass *libregraph.EducationClass
currentUser = &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "user",
},
}
)
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
identityEducationBackend = &identitymocks.EducationBackend{}
identityBackend = &identitymocks.Backend{}
gatewayClient = &mocks.GatewayClient{}
newClass = libregraph.NewEducationClass("math", "course")
newClass.SetMembersodataBind([]string{"/users/user1"})
newClass.SetId("math")
rr = httptest.NewRecorder()
ctx = context.Background()
cfg = defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
svc, _ = service.NewService(
service.Config(cfg),
service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
service.WithIdentityEducationBackend(identityEducationBackend),
service.Logger(log.NewLogger(log.Level("debug"))),
)
})
Describe("GetEducationClasses", func() {
It("handles invalid ODATA parameters", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes?§foo=bar", nil)
svc.GetEducationClasses(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles invalid sorting queries", func() {
identityEducationBackend.On("GetEducationClasses", ctx, mock.Anything).Return([]*libregraph.EducationClass{newClass}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes?$orderby=invalid", nil)
svc.GetEducationClasses(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
err = json.Unmarshal(data, &odataerr)
Expect(err).ToNot(HaveOccurred())
Expect(odataerr.Error.Code).To(Equal("invalidRequest"))
})
It("handles unknown backend errors", func() {
identityEducationBackend.On("GetEducationClasses", ctx, mock.Anything).Return(nil, errors.New("failed"))
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
svc.GetEducationClasses(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
err = json.Unmarshal(data, &odataerr)
Expect(err).ToNot(HaveOccurred())
Expect(odataerr.Error.Code).To(Equal("generalException"))
})
It("handles backend errors", func() {
identityEducationBackend.On("GetEducationClasses", ctx, mock.Anything).Return(nil, errorcode.New(errorcode.AccessDenied, "access denied"))
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
svc.GetEducationClasses(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
err = json.Unmarshal(data, &odataerr)
Expect(err).ToNot(HaveOccurred())
Expect(odataerr.Error.Code).To(Equal("accessDenied"))
})
It("renders an empty list of classes", func() {
identityEducationBackend.On("GetEducationClasses", ctx, mock.Anything).Return([]*libregraph.EducationClass{}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
svc.GetEducationClasses(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := service.ListResponse{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(res.Value).To(Equal([]interface{}{}))
})
It("renders a list of classes", func() {
identityEducationBackend.On("GetEducationClasses", ctx, mock.Anything).Return([]*libregraph.EducationClass{newClass}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
svc.GetEducationClasses(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := groupList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
Expect(res.Value[0].GetId()).To(Equal("math"))
})
})
Describe("GetEducationClass", func() {
It("handles missing or empty class id", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
svc.GetEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", "")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
svc.GetEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
Context("with an existing class", func() {
BeforeEach(func() {
identityEducationBackend.On("GetEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(newClass, nil)
})
It("gets the class", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes/"+*newClass.Id, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
svc.GetEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
})
})
})
Describe("PostEducationClass", func() {
It("handles invalid body", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/", bytes.NewBufferString("{invalid"))
svc.PostEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing display name", func() {
newClass = libregraph.NewEducationClassWithDefaults()
newClass.SetId("disallowed")
newClass.SetMembersodataBind([]string{"/non-users/user"})
newClassJson, err := json.Marshal(newClass)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/", bytes.NewBuffer(newClassJson))
svc.PostEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("disallows group create ids", func() {
newClass = libregraph.NewEducationClassWithDefaults()
newClass.SetId("disallowed")
newClass.SetDisplayName("New Class")
newClass.SetMembersodataBind([]string{"/non-users/user"})
newClassJson, err := json.Marshal(newClass)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/", bytes.NewBuffer(newClassJson))
svc.PostEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("creates the Class", func() {
newClass = libregraph.NewEducationClassWithDefaults()
newClass.SetDisplayName("New Class")
newClassJson, err := json.Marshal(newClass)
Expect(err).ToNot(HaveOccurred())
identityEducationBackend.On("CreateEducationClass", mock.Anything, mock.Anything).Return(newClass, nil)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/class/", bytes.NewBuffer(newClassJson))
svc.PostEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusCreated))
})
})
Describe("PatchEducationClass", func() {
It("handles invalid body", func() {
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes/", bytes.NewBufferString("{invalid"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing or empty group id", func() {
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", nil)
svc.PatchEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
r = httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", "")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
Context("with an existing group", func() {
BeforeEach(func() {
identityEducationBackend.On("GetEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(newClass, nil)
})
It("fails when the number of users is exceeded - spec says 20 max", func() {
updatedClass := libregraph.NewEducationClassWithDefaults()
updatedClass.SetDisplayName("class updated")
updatedClass.SetMembersodataBind([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
"19", "20", "21"})
updatedClassJson, err := json.Marshal(updatedClass)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", bytes.NewBuffer(updatedClassJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
resp, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(resp)).To(ContainSubstring("Request is limited to 20"))
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("succeeds when the number of users is over 20 but the limit is raised to 21", func() {
updatedClass := libregraph.NewEducationClassWithDefaults()
updatedClass.SetDisplayName("group1 updated")
updatedClass.SetMembersodataBind([]string{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
"19", "20", "21",
})
updatedClassJson, err := json.Marshal(updatedClass)
Expect(err).ToNot(HaveOccurred())
cfg.API.GroupMembersPatchLimit = 21
svc, _ = service.NewService(
service.Config(cfg),
service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
)
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", bytes.NewBuffer(updatedClassJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
resp, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(resp)).To(ContainSubstring("Error parsing member@odata.bind values"))
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on invalid user refs", func() {
updatedClass := libregraph.NewEducationClassWithDefaults()
updatedClass.SetDisplayName("class updated")
updatedClass.SetMembersodataBind([]string{"invalid"})
updatedClassJson, err := json.Marshal(updatedClass)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", bytes.NewBuffer(updatedClassJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails when the adding non-users users", func() {
updatedClass := libregraph.NewEducationClassWithDefaults()
updatedClass.SetDisplayName("group1 updated")
updatedClass.SetMembersodataBind([]string{"/non-users/user1"})
updatedClassJson, err := json.Marshal(updatedClass)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", bytes.NewBuffer(updatedClassJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("adds members to the class", func() {
identityBackend.On("AddMembersToGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
updatedClass := libregraph.NewEducationClassWithDefaults()
updatedClass.SetDisplayName("Class updated")
updatedClass.SetMembersodataBind([]string{"/users/user1"})
updatedClassJson, err := json.Marshal(updatedClass)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", bytes.NewBuffer(updatedClassJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToGroup", 1)
})
})
})
Describe("DeleteEducationClass", func() {
Context("with an existing EducationClass", func() {
BeforeEach(func() {
identityEducationBackend.On("GetEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(newClass, nil)
})
})
It("deletes the EducationClass", func() {
identityEducationBackend.On("DeleteEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/classes", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClass(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "DeleteEducationClass", 1)
//eventsPublisher.AssertNumberOfCalls(GinkgoT(), "Publish", 1)
})
})
Describe("GetEducationClassMembers", func() {
It("gets the list of members", func() {
user := libregraph.NewEducationUser()
user.SetId("user")
identityEducationBackend.On("GetEducationClassMembers", mock.Anything, mock.Anything, mock.Anything).
Return([]*libregraph.EducationUser{user}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes/{classID}/members", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.GetEducationClassMembers(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
var members []*libregraph.EducationUser
err = json.Unmarshal(data, &members)
Expect(err).ToNot(HaveOccurred())
Expect(len(members)).To(Equal(1))
Expect(members[0].GetId()).To(Equal("user"))
})
})
Describe("PostEducationClassMember", func() {
It("fails on invalid body", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/members", bytes.NewBufferString("{invalid"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on missing member refs", func() {
member := libregraph.NewMemberReference()
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/members", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on invalid member refs", func() {
member := libregraph.NewMemberReference()
member.SetOdataId("/invalidtype/user")
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/members", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("adds a new member", func() {
member := libregraph.NewMemberReference()
member.SetOdataId("/users/user")
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
identityBackend.On("AddMembersToGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/members", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToGroup", 1)
})
})
Describe("DeleteEducationClassMembers", func() {
It("handles missing or empty member id", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/members/{memberID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing or empty member id", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/members/{memberID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("memberID", "/users/user")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("deletes members", func() {
identityBackend.On("RemoveMemberFromGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/members/{memberID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
rctx.URLParams.Add("memberID", "/users/user1")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClassMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityBackend.AssertNumberOfCalls(GinkgoT(), "RemoveMemberFromGroup", 1)
})
})
})

View File

@@ -127,7 +127,7 @@ func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) {
logger.Debug().
Int("number", len(memberRefs)).
Int("limit", g.config.API.GroupMembersPatchLimit).
Msg("could not create group, exceeded members limit")
Msg("could not add group members, exceeded members limit")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest,
fmt.Sprintf("Request is limited to %d members", g.config.API.GroupMembersPatchLimit))
return

View File

@@ -139,6 +139,46 @@ func (i instrument) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Req
i.next.DeleteEducationSchoolUser(w, r)
}
// GetEducationClasses implements the Service interface.
func (i instrument) GetEducationClasses(w http.ResponseWriter, r *http.Request) {
i.next.GetEducationClasses(w, r)
}
// GetEducationClass implements the Service interface.
func (i instrument) GetEducationClass(w http.ResponseWriter, r *http.Request) {
i.next.GetEducationClass(w, r)
}
// PostEducationClass implements the Service interface.
func (i instrument) PostEducationClass(w http.ResponseWriter, r *http.Request) {
i.next.PostEducationClass(w, r)
}
// PatchEducationClass implements the Service interface.
func (i instrument) PatchEducationClass(w http.ResponseWriter, r *http.Request) {
i.next.PatchEducationClass(w, r)
}
// DeleteEducationClass implements the Service interface.
func (i instrument) DeleteEducationClass(w http.ResponseWriter, r *http.Request) {
i.next.DeleteEducationClass(w, r)
}
// GetEducationClassMembers implements the Service interface.
func (i instrument) GetEducationClassMembers(w http.ResponseWriter, r *http.Request) {
i.next.GetEducationClassMembers(w, r)
}
// PostEducationClassMember implements the Service interface.
func (i instrument) PostEducationClassMember(w http.ResponseWriter, r *http.Request) {
i.next.PostEducationClassMember(w, r)
}
// DeleteEducationClassMember implements the Service interface.
func (i instrument) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) {
i.next.DeleteEducationClassMember(w, r)
}
// GetEducationUsers implements the Service interface.
func (i instrument) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
i.next.GetEducationUsers(w, r)

View File

@@ -139,6 +139,46 @@ func (l logging) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Reques
l.next.DeleteEducationSchoolUser(w, r)
}
// GetEducationClasses implements the Service interface.
func (l logging) GetEducationClasses(w http.ResponseWriter, r *http.Request) {
l.next.GetEducationClasses(w, r)
}
// GetEducationClass implements the Service interface.
func (l logging) GetEducationClass(w http.ResponseWriter, r *http.Request) {
l.next.GetEducationClass(w, r)
}
// PostEducationClass implements the Service interface.
func (l logging) PostEducationClass(w http.ResponseWriter, r *http.Request) {
l.next.PostEducationClass(w, r)
}
// PatchEducationClass implements the Service interface.
func (l logging) PatchEducationClass(w http.ResponseWriter, r *http.Request) {
l.next.PatchEducationClass(w, r)
}
// DeleteEducationClass implements the Service interface.
func (l logging) DeleteEducationClass(w http.ResponseWriter, r *http.Request) {
l.next.DeleteEducationClass(w, r)
}
// GetEducationClassMembers implements the Service interface.
func (l logging) GetEducationClassMembers(w http.ResponseWriter, r *http.Request) {
l.next.GetEducationClassMembers(w, r)
}
// PostEducationClassMember implements the Service interface.
func (l logging) PostEducationClassMember(w http.ResponseWriter, r *http.Request) {
l.next.PostEducationClassMember(w, r)
}
// DeleteEducationClassMember implements the Service interface.
func (l logging) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) {
l.next.DeleteEducationClassMember(w, r)
}
// GetEducationUsers implements the Service interface.
func (l logging) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
l.next.GetEducationUsers(w, r)

View File

@@ -58,11 +58,20 @@ type Service interface {
PostEducationSchoolUser(http.ResponseWriter, *http.Request)
DeleteEducationSchoolUser(http.ResponseWriter, *http.Request)
GetEducationClasses(http.ResponseWriter, *http.Request)
GetEducationClass(http.ResponseWriter, *http.Request)
PostEducationClass(http.ResponseWriter, *http.Request)
PatchEducationClass(http.ResponseWriter, *http.Request)
DeleteEducationClass(w http.ResponseWriter, r *http.Request)
GetEducationClassMembers(w http.ResponseWriter, r *http.Request)
PostEducationClassMember(w http.ResponseWriter, r *http.Request)
GetEducationUsers(http.ResponseWriter, *http.Request)
GetEducationUser(http.ResponseWriter, *http.Request)
PostEducationUser(http.ResponseWriter, *http.Request)
DeleteEducationUser(http.ResponseWriter, *http.Request)
PatchEducationUser(http.ResponseWriter, *http.Request)
DeleteEducationClassMember(w http.ResponseWriter, r *http.Request)
GetDrives(w http.ResponseWriter, r *http.Request)
GetSingleDrive(w http.ResponseWriter, r *http.Request)
@@ -293,16 +302,16 @@ func NewService(opts ...Option) (Graph, error) {
})
})
r.Route("/classes", func(r chi.Router) {
r.Get("/", svc.GetGroups)
r.Post("/", svc.PostGroup)
r.Route("/{groupID}", func(r chi.Router) {
r.Get("/", svc.GetGroup)
r.Delete("/", svc.DeleteGroup)
r.Patch("/", svc.PatchGroup)
r.Get("/", svc.GetEducationClasses)
r.Post("/", svc.PostEducationClass)
r.Route("/{classID}", func(r chi.Router) {
r.Get("/", svc.GetEducationClass)
r.Delete("/", svc.DeleteEducationClass)
r.Patch("/", svc.PatchEducationClass)
r.Route("/members", func(r chi.Router) {
r.Get("/", svc.GetGroupMembers)
r.Post("/$ref", svc.PostGroupMember)
r.Delete("/{memberID}/$ref", svc.DeleteGroupMember)
r.Get("/", svc.GetEducationClassMembers)
r.Post("/$ref", svc.PostEducationClassMember)
r.Delete("/{memberID}/$ref", svc.DeleteEducationClassMember)
})
})
})

View File

@@ -135,6 +135,46 @@ func (t tracing) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Reques
t.next.DeleteEducationSchoolUser(w, r)
}
// GetEducationClasses implements the Service interface.
func (t tracing) GetEducationClasses(w http.ResponseWriter, r *http.Request) {
t.next.GetEducationClasses(w, r)
}
// GetEducationClass implements the Service interface.
func (t tracing) GetEducationClass(w http.ResponseWriter, r *http.Request) {
t.next.GetEducationClass(w, r)
}
// PostEducationClass implements the Service interface.
func (t tracing) PostEducationClass(w http.ResponseWriter, r *http.Request) {
t.next.PostEducationClass(w, r)
}
// PatchEducationClass implements the Service interface.
func (t tracing) PatchEducationClass(w http.ResponseWriter, r *http.Request) {
t.next.PatchEducationClass(w, r)
}
// DeleteEducationClass implements the Service interface.
func (t tracing) DeleteEducationClass(w http.ResponseWriter, r *http.Request) {
t.next.DeleteEducationClass(w, r)
}
// GetEducationClassMembers implements the Service interface.
func (t tracing) GetEducationClassMembers(w http.ResponseWriter, r *http.Request) {
t.next.GetEducationClassMembers(w, r)
}
// PostEducationClassMember implements the Service interface.
func (t tracing) PostEducationClassMember(w http.ResponseWriter, r *http.Request) {
t.next.PostEducationClassMember(w, r)
}
// DeleteEducationClassMember implements the Service interface.
func (t tracing) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) {
t.next.DeleteEducationClassMember(w, r)
}
// GetEducationUsers implements the Service interface.
func (t tracing) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
t.next.GetEducationUsers(w, r)