From 8e1a65fc292c2f6892edf332ef1c6ca96cc419bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sw=C3=A4rd?= Date: Mon, 6 Feb 2023 12:16:04 +0100 Subject: [PATCH] graph: Add support for listing/adding/removing teachers to a class --- services/graph/pkg/identity/backend.go | 7 + services/graph/pkg/identity/err_education.go | 15 ++ .../pkg/identity/ldap_education_class.go | 175 ++++++++++++++++++ .../pkg/identity/mocks/education_backend.go | 51 +++++ .../graph/pkg/service/v0/educationclasses.go | 162 ++++++++++++++++ .../pkg/service/v0/educationclasses_test.go | 115 ++++++++++++ services/graph/pkg/service/v0/instrument.go | 15 ++ services/graph/pkg/service/v0/logging.go | 15 ++ services/graph/pkg/service/v0/service.go | 9 + services/graph/pkg/service/v0/tracing.go | 15 ++ 10 files changed, 579 insertions(+) diff --git a/services/graph/pkg/identity/backend.go b/services/graph/pkg/identity/backend.go index 42aaea0525..4d2679773f 100644 --- a/services/graph/pkg/identity/backend.go +++ b/services/graph/pkg/identity/backend.go @@ -91,6 +91,13 @@ type EducationBackend interface { GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) // GetEducationUsers lists all education users GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) + + // GetEducationClassTeachers returns the EducationUser teachers for an EducationClass + GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) + // AddTeacherToEducationclass adds a teacher (by ID) to class in the identity backend. + AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error + // RemoveTeacherFromEducationClass removes teacher (by ID) from a class + RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error } func CreateUserModelFromCS3(u *cs3.User) *libregraph.User { diff --git a/services/graph/pkg/identity/err_education.go b/services/graph/pkg/identity/err_education.go index e68e2e030c..297ca88696 100644 --- a/services/graph/pkg/identity/err_education.go +++ b/services/graph/pkg/identity/err_education.go @@ -118,3 +118,18 @@ func (i *ErrEducationBackend) GetEducationUser(ctx context.Context, nameOrID str func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) { return nil, errNotImplemented } + +// GetEducationClassTeachers implements the EducationBackend interface for the ErrEducationBackend backend. +func (i *ErrEducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) { + return nil, errNotImplemented +} + +// AddTeacherToEducationclass implements the EducationBackend interface for the ErrEducationBackend backend. +func (i *ErrEducationBackend) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error { + return errNotImplemented +} + +// RemoveTeacherFromEducationClass implements the EducationBackend interface for the ErrEducationBackend backend. +func (i *ErrEducationBackend) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error { + return errNotImplemented +} diff --git a/services/graph/pkg/identity/ldap_education_class.go b/services/graph/pkg/identity/ldap_education_class.go index 3ea687c462..cb55178713 100644 --- a/services/graph/pkg/identity/ldap_education_class.go +++ b/services/graph/pkg/identity/ldap_education_class.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/go-ldap/ldap/v3" + "github.com/libregraph/idm/pkg/ldapdn" libregraph "github.com/owncloud/libre-graph-api-go" oldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap" "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" @@ -14,12 +15,14 @@ import ( type educationClassAttributeMap struct { externalID string classification string + teachers string } func newEducationClassAttributeMap() educationClassAttributeMap { return educationClassAttributeMap{ externalID: "ocEducationExternalId", classification: "ocEducationClassType", + teachers: "ocEducationTeacherMember", } } @@ -283,6 +286,7 @@ func (i *LDAP) getEducationClassAttrTypes(requestMembers bool) []string { i.educationConfig.classAttributeMap.classification, i.educationConfig.classAttributeMap.externalID, i.educationConfig.memberOfSchoolAttribute, + i.educationConfig.classAttributeMap.teachers, } if requestMembers { attrs = append(attrs, i.groupAttributeMap.member) @@ -337,3 +341,174 @@ func (i *LDAP) getEducationClassByID(nameOrID string, requestMembers bool) (*lda i.getEducationClassAttrTypes(requestMembers), ) } + +// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass +func (i *LDAP) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + class, err := i.getEducationClassByID(classID, false) + if err != nil { + logger.Debug().Err(err).Msg("could not get class: backend error") + return nil, err + } + + teacherEntries, err := i.expandLDAPClassTeachers(ctx, class) + result := make([]*libregraph.EducationUser, 0, len(teacherEntries)) + if err != nil { + return nil, err + } + for _, teacher := range teacherEntries { + if u := i.createEducationUserModelFromLDAP(teacher); u != nil { + result = append(result, u) + } + } + + return result, nil + +} + +func (i *LDAP) expandLDAPClassTeachers(ctx context.Context, e *ldap.Entry) ([]*ldap.Entry, error) { + logger := i.logger.SubloggerWithRequestID(ctx) + logger.Debug().Str("backend", "ldap").Msg("expandLDAPClassTeachers") + result := []*ldap.Entry{} + + for _, teacherDN := range e.GetEqualFoldAttributeValues(i.educationConfig.classAttributeMap.teachers) { + if teacherDN == "" { + continue + } + logger.Debug().Str("teacherDN", teacherDN).Msg("lookup") + ue, err := i.getUserByDN(teacherDN) + if err != nil { + // Ignore errors when reading a specific teacher fails, just log them and continue + logger.Debug().Err(err).Str("teacher", teacherDN).Msg("error reading class teacher") + continue + } + result = append(result, ue) + } + + return result, nil +} + +// AddTeacherToEducationclass adds a teacher (by ID) to class in the identity backend. +func (i *LDAP) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error { + logger := i.logger.SubloggerWithRequestID(ctx) + class, err := i.getEducationClassByID(classID, false) + if err != nil { + logger.Debug().Err(err).Msg("could not get class: backend error") + return err + } + + logger.Debug().Str("classDn", class.DN).Msg("got a class") + teacher, err := i.getEducationUserByNameOrID(teacherID) + + if err != nil { + logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend") + return err + } + + logger.Debug().Str("userDn", teacher.DN).Msg("got a user") + + mr := ldap.ModifyRequest{DN: class.DN} + // Handle empty teacher list + current := class.GetEqualFoldAttributeValues(i.educationConfig.classAttributeMap.teachers) + if len(current) == 1 && current[0] == "" { + mr.Delete(i.educationConfig.classAttributeMap.teachers, []string{""}) + } + + // Create a Set of current teachers + currentSet := make(map[string]struct{}, len(current)) + for _, currentTeacher := range current { + if currentTeacher == "" { + continue + } + nCurrentTeacher, err := ldapdn.ParseNormalize(currentTeacher) + if err != nil { + // Couldn't parse teacher value as a DN, skipping + logger.Warn().Str("teacherDN", currentTeacher).Err(err).Msg("Couldn't parse DN") + continue + } + currentSet[nCurrentTeacher] = struct{}{} + } + + var newTeacherDN []string + nDN, err := ldapdn.ParseNormalize(teacher.DN) + if err != nil { + logger.Error().Str("new teacher", teacher.DN).Err(err).Msg("Couldn't parse DN") + return err + } + if _, present := currentSet[nDN]; !present { + newTeacherDN = append(newTeacherDN, teacher.DN) + } else { + logger.Debug().Str("teacherDN", teacher.DN).Msg("Member already present in group. Skipping") + } + + if len(newTeacherDN) > 0 { + mr.Add(i.educationConfig.classAttributeMap.teachers, newTeacherDN) + + if err := i.conn.Modify(&mr); err != nil { + return err + } + } + + return nil +} + +// RemoveTeacherFromEducationClass removes teacher (by ID) from a class +func (i *LDAP) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error { + logger := i.logger.SubloggerWithRequestID(ctx) + class, err := i.getEducationClassByID(classID, false) + if err != nil { + logger.Debug().Err(err).Msg("could not get class: backend error") + return err + } + + teacher, err := i.getEducationUserByNameOrID(teacherID) + if err != nil { + logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend") + return err + } + + if mr, err := i.removeTeacherFromClassEntry(class, teacher.DN); err == nil { + return i.conn.Modify(mr) + } + + return nil +} + +// removeTeacherFromClassEntry creates an LDAP Modify request (not sending it) +// that would update the supplied entry to remove the specified teacher from the +// class +func (i *LDAP) removeTeacherFromClassEntry(class *ldap.Entry, teacherDN string) (*ldap.ModifyRequest, error) { + nOldTeacherDN, err := ldapdn.ParseNormalize(teacherDN) + if err != nil { + return nil, err + } + teachers := class.GetEqualFoldAttributeValues(i.educationConfig.classAttributeMap.teachers) + found := false + for _, teacher := range teachers { + if teacher == "" { + continue + } + if nTeacher, err := ldapdn.ParseNormalize(teacher); err != nil { + // We couldn't parse the teacher value as a DN. Let's keep it + // as it is but log a warning + i.logger.Warn().Str("teacherDN", teacher).Err(err).Msg("Couldn't parse DN") + continue + } else { + if nTeacher == nOldTeacherDN { + found = true + } + } + } + if !found { + i.logger.Debug().Str("backend", "ldap").Str("groupdn", class.DN).Str("teacher", teacherDN). + Msg("The target is not a teacher of the class") + return nil, ErrNotFound + } + + mr := ldap.ModifyRequest{DN: class.DN} + if len(teachers) == 1 { + mr.Add(i.educationConfig.classAttributeMap.teachers, []string{""}) + } + mr.Delete(i.educationConfig.classAttributeMap.teachers, []string{teacherDN}) + return &mr, nil +} diff --git a/services/graph/pkg/identity/mocks/education_backend.go b/services/graph/pkg/identity/mocks/education_backend.go index 42e7cbdbaf..1de863c454 100644 --- a/services/graph/pkg/identity/mocks/education_backend.go +++ b/services/graph/pkg/identity/mocks/education_backend.go @@ -29,6 +29,20 @@ func (_m *EducationBackend) AddClassesToEducationSchool(ctx context.Context, sch return r0 } +// AddTeacherToEducationClass provides a mock function with given fields: ctx, classID, teacherID +func (_m *EducationBackend) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error { + ret := _m.Called(ctx, classID, teacherID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, classID, teacherID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // AddUsersToEducationSchool provides a mock function with given fields: ctx, schoolID, memberID func (_m *EducationBackend) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error { ret := _m.Called(ctx, schoolID, memberID) @@ -200,6 +214,29 @@ func (_m *EducationBackend) GetEducationClassMembers(ctx context.Context, nameOr return r0, r1 } +// GetEducationClassTeachers provides a mock function with given fields: ctx, classID +func (_m *EducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) { + ret := _m.Called(ctx, classID) + + var r0 []*libregraph.EducationUser + if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.EducationUser); ok { + r0 = rf(ctx, classID) + } 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, classID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetEducationClasses provides a mock function with given fields: ctx func (_m *EducationBackend) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) { ret := _m.Called(ctx) @@ -375,6 +412,20 @@ func (_m *EducationBackend) RemoveClassFromEducationSchool(ctx context.Context, return r0 } +// RemoveTeacherFromEducationClass provides a mock function with given fields: ctx, classID, teacherID +func (_m *EducationBackend) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error { + ret := _m.Called(ctx, classID, teacherID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, classID, teacherID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // RemoveUserFromEducationSchool provides a mock function with given fields: ctx, schoolID, memberID func (_m *EducationBackend) RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error { ret := _m.Called(ctx, schoolID, memberID) diff --git a/services/graph/pkg/service/v0/educationclasses.go b/services/graph/pkg/service/v0/educationclasses.go index 989a8be7f6..0591190f37 100644 --- a/services/graph/pkg/service/v0/educationclasses.go +++ b/services/graph/pkg/service/v0/educationclasses.go @@ -443,6 +443,168 @@ func (g Graph) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request render.NoContent(w, r) } +// GetEducationClassTeachers implements the Service interface. +func (g Graph) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("calling get class teachers") + classID := chi.URLParam(r, "classID") + classID, err := url.PathUnescape(classID) + if err != nil { + logger.Debug().Str("id", classID).Msg("could not get class teachers: 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 teachers: missing class id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id") + return + } + + logger.Debug().Str("id", classID).Msg("calling get class teachers on backend") + teachers, err := g.identityEducationBackend.GetEducationClassTeachers(r.Context(), classID) + // teachers, err := g.identityEducationBackend.GetEducationClassMembers(r.Context(), classID) + if err != nil { + logger.Debug().Err(err).Msg("could not get class teachers: 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, teachers) +} + +// PostEducationClassTeacher implements the Service interface. +func (g Graph) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("Calling post class teacher") + + classID := chi.URLParam(r, "classID") + classID, err := url.PathUnescape(classID) + if err != nil { + logger.Debug(). + Err(err). + Str("id", classID). + Msg("could not add teacher 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 teacher: 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 teacher: 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 teacher: @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 teacher: 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 teachers") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as class teachers") + return + } + + logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add teacher on backend") + err = g.identityEducationBackend.AddTeacherToEducationClass(r.Context(), classID, id) + + if err != nil { + logger.Debug().Err(err).Msg("could not add class teacher: 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.EducationClassTeacherAdded{Executant: currentUser.Id, EducationClassID: classID, UserID: id}) + */ + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + +// DeleteEducationClassTeacher implements the Service interface. +func (g Graph) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("calling delete class teacher") + + 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 teacher: 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 teacher: missing class id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id") + return + } + + teacherID := chi.URLParam(r, "teacherID") + teacherID, err = url.PathUnescape(teacherID) + if err != nil { + logger.Debug().Err(err).Str("id", teacherID).Msg("could not delete class teacher: unescaping teacher id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping teacher id failed") + return + } + + if teacherID == "" { + logger.Debug().Msg("could not delete class teacher: missing teacher id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing teacher id") + return + } + logger.Debug().Str("classID", classID).Str("teacherID", teacherID).Msg("calling delete teacher on backend") + err = g.identityEducationBackend.RemoveTeacherFromEducationClass(r.Context(), classID, teacherID) + + if err != nil { + logger.Debug().Err(err).Msg("could not delete class teacher: 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.EducationClassTeacherRemoved{Executant: currentUser.Id, EducationClassID: classID, UserID: teacherID}) + */ + 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 diff --git a/services/graph/pkg/service/v0/educationclasses_test.go b/services/graph/pkg/service/v0/educationclasses_test.go index ba90311bd2..b8db6df100 100644 --- a/services/graph/pkg/service/v0/educationclasses_test.go +++ b/services/graph/pkg/service/v0/educationclasses_test.go @@ -530,4 +530,119 @@ var _ = Describe("EducationClass", func() { identityBackend.AssertNumberOfCalls(GinkgoT(), "RemoveMemberFromGroup", 1) }) }) + + Describe("GetEducationClassTeachers", func() { + It("gets the list of teachers", func() { + user := libregraph.NewEducationUser() + user.SetId("user") + identityEducationBackend.On("GetEducationClassTeachers", mock.Anything, mock.Anything, mock.Anything). + Return([]*libregraph.EducationUser{user}, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes/{classID}/teachers", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("classID", *newClass.Id) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.GetEducationClassTeachers(rr, r) + Expect(rr.Code).To(Equal(http.StatusOK)) + + data, err := io.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + var teachers []*libregraph.EducationUser + err = json.Unmarshal(data, &teachers) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(teachers)).To(Equal(1)) + Expect(teachers[0].GetId()).To(Equal("user")) + }) + }) + + Describe("PostEducationClassTeacher", func() { + It("fails on invalid body", func() { + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/teachers", 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.PostEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("fails on missing teacher 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}/teachers", 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.PostEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("fails on invalid teacher 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}/teachers", 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.PostEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("adds a new teacher", func() { + member := libregraph.NewMemberReference() + member.SetOdataId("/users/user") + data, err := json.Marshal(member) + Expect(err).ToNot(HaveOccurred()) + identityEducationBackend.On("AddTeacherToEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/teachers", 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.PostEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusNoContent)) + + identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddTeacherToEducationClass", 1) + }) + }) + + Describe("DeleteEducationClassTeacher", func() { + It("handles missing or empty teacher id", func() { + r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/teachers/{teacherID}/$ref", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("classID", *newClass.Id) + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.DeleteEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("handles missing or empty teacher id", func() { + r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/teachers/{teacherID}/$ref", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("teacherID", "/users/user") + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.DeleteEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("deletes teacher", func() { + identityEducationBackend.On("RemoveTeacherFromEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/teachers/{teacherID}/$ref", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("classID", *newClass.Id) + rctx.URLParams.Add("teacherID", "/users/user1") + r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.DeleteEducationClassTeacher(rr, r) + Expect(rr.Code).To(Equal(http.StatusNoContent)) + + identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveTeacherFromEducationClass", 1) + }) + }) }) diff --git a/services/graph/pkg/service/v0/instrument.go b/services/graph/pkg/service/v0/instrument.go index 1defd6210b..18125ece96 100644 --- a/services/graph/pkg/service/v0/instrument.go +++ b/services/graph/pkg/service/v0/instrument.go @@ -293,3 +293,18 @@ func (i instrument) AssignTags(w http.ResponseWriter, r *http.Request) { func (i instrument) UnassignTags(w http.ResponseWriter, r *http.Request) { i.next.UnassignTags(w, r) } + +// GetEducationClassTeachers implements the Service interface. +func (i instrument) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) { + i.next.UnassignTags(w, r) +} + +// PostEducationClassTeacher implements the Service interface. +func (i instrument) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + i.next.UnassignTags(w, r) +} + +// DeleteEducationClassTeacher implements the Service interface. +func (i instrument) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + i.next.UnassignTags(w, r) +} diff --git a/services/graph/pkg/service/v0/logging.go b/services/graph/pkg/service/v0/logging.go index 818d1d2e03..82156c9019 100644 --- a/services/graph/pkg/service/v0/logging.go +++ b/services/graph/pkg/service/v0/logging.go @@ -293,3 +293,18 @@ func (l logging) AssignTags(w http.ResponseWriter, r *http.Request) { func (l logging) UnassignTags(w http.ResponseWriter, r *http.Request) { l.next.UnassignTags(w, r) } + +// GetEducationClassTeachers implements the Service interface. +func (l logging) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) { + l.next.UnassignTags(w, r) +} + +// PostEducationClassTeacher implements the Service interface. +func (l logging) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + l.next.UnassignTags(w, r) +} + +// DeleteEducationClassTeacher implements the Service interface. +func (l logging) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + l.next.UnassignTags(w, r) +} diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 5de44badb3..0d4cb643ca 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -84,6 +84,10 @@ type Service interface { PatchEducationUser(http.ResponseWriter, *http.Request) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) + GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) + PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) + DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) + GetDrives(w http.ResponseWriter, r *http.Request) GetSingleDrive(w http.ResponseWriter, r *http.Request) GetAllDrives(w http.ResponseWriter, r *http.Request) @@ -270,6 +274,11 @@ func NewService(opts ...Option) (Graph, error) { r.Post("/$ref", svc.PostEducationClassMember) r.Delete("/{memberID}/$ref", svc.DeleteEducationClassMember) }) + r.Route("/teachers", func(r chi.Router) { + r.Get("/", svc.GetEducationClassTeachers) + r.Post("/$ref", svc.PostEducationClassTeacher) + r.Delete("/{teacherID}/$ref", svc.DeleteEducationClassTeacher) + }) }) }) }) diff --git a/services/graph/pkg/service/v0/tracing.go b/services/graph/pkg/service/v0/tracing.go index 10505a5b43..ad4a6b5c7d 100644 --- a/services/graph/pkg/service/v0/tracing.go +++ b/services/graph/pkg/service/v0/tracing.go @@ -289,3 +289,18 @@ func (t tracing) AssignTags(w http.ResponseWriter, r *http.Request) { func (t tracing) UnassignTags(w http.ResponseWriter, r *http.Request) { t.next.UnassignTags(w, r) } + +// GetEducationClassTeachers implements the Service interface. +func (t tracing) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) { + t.next.UnassignTags(w, r) +} + +// PostEducationClassTeacher implements the Service interface. +func (t tracing) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + t.next.UnassignTags(w, r) +} + +// DeleteEducationClassTeacher implements the Service interface. +func (t tracing) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) { + t.next.UnassignTags(w, r) +}