mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-18 03:24:01 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
33
services/graph/pkg/identity/ldap_education_class.go
Normal file
33
services/graph/pkg/identity/ldap_education_class.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
433
services/graph/pkg/service/v0/educationclasses.go
Normal file
433
services/graph/pkg/service/v0/educationclasses.go
Normal 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
|
||||
}
|
||||
530
services/graph/pkg/service/v0/educationclasses_test.go
Normal file
530
services/graph/pkg/service/v0/educationclasses_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user