package svc_test import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "net/url" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/go-chi/chi/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" "github.com/stretchr/testify/mock" "google.golang.org/grpc" libregraph "github.com/opencloud-eu/libre-graph-api-go" "github.com/qsfera/server/pkg/shared" "github.com/qsfera/server/services/graph/pkg/config" "github.com/qsfera/server/services/graph/pkg/config/defaults" "github.com/qsfera/server/services/graph/pkg/errorcode" identitymocks "github.com/qsfera/server/services/graph/pkg/identity/mocks" service "github.com/qsfera/server/services/graph/pkg/service/v0" ) type schoolList struct { Value []*libregraph.EducationSchool } var _ = Describe("Schools", func() { var ( svc service.Service ctx context.Context cfg *config.Config gatewayClient *cs3mocks.GatewayAPIClient gatewaySelector pool.Selectable[gateway.GatewayAPIClient] identityEducationBackend *identitymocks.EducationBackend rr *httptest.ResponseRecorder newSchool *libregraph.EducationSchool currentUser = &userv1beta1.User{ Id: &userv1beta1.UserId{ OpaqueId: "user", }, } ) BeforeEach(func() { pool.RemoveSelector("GatewaySelector" + "qsfera.api.gateway") gatewayClient = &cs3mocks.GatewayAPIClient{} gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient]( "GatewaySelector", "qsfera.api.gateway", func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient { return gatewayClient }, ) identityEducationBackend = &identitymocks.EducationBackend{} newSchool = libregraph.NewEducationSchool() newSchool.SetId("school1") 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.Identity.LDAP.EducationConfig.SchoolTerminationGraceDays = 30 cfg.TokenManager.JWTSecret = "loremipsum" cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} var err error svc, err = service.NewService( service.Config(cfg), service.WithGatewaySelector(gatewaySelector), service.WithIdentityEducationBackend(identityEducationBackend), ) Expect(err).ToNot(HaveOccurred()) }) Describe("GetEducationSchools", func() { It("handles invalid ODATA parameters", func() { r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?§foo=bar", nil) svc.GetEducationSchools(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("handles invalid sorting queries", func() { identityEducationBackend.On("GetEducationSchools", ctx, mock.Anything).Return([]*libregraph.EducationSchool{newSchool}, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?$orderby=invalid", nil) svc.GetEducationSchools(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("GetEducationSchools", ctx, mock.Anything).Return(nil, errors.New("failed")) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil) svc.GetEducationSchools(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("GetEducationSchools", ctx, mock.Anything).Return(nil, errorcode.New(errorcode.AccessDenied, "access denied")) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil) svc.GetEducationSchools(rr, r) Expect(rr.Code).To(Equal(http.StatusForbidden)) 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 schools", func() { identityEducationBackend.On("GetEducationSchools", ctx, mock.Anything).Return([]*libregraph.EducationSchool{}, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil) svc.GetEducationSchools(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([]any{})) }) It("renders a list of schools", func() { identityEducationBackend.On("GetEducationSchools", ctx, mock.Anything).Return([]*libregraph.EducationSchool{newSchool}, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil) svc.GetEducationSchools(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) data, err := io.ReadAll(rr.Body) Expect(err).ToNot(HaveOccurred()) res := schoolList{} err = json.Unmarshal(data, &res) Expect(err).ToNot(HaveOccurred()) Expect(len(res.Value)).To(Equal(1)) Expect(res.Value[0].GetId()).To(Equal("school1")) }) When("used with a filter", func() { It("fails with an unsupported filter", func() { svc.GetEducationSchools(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?$filter="+url.QueryEscape("displayName ne 'test'"), nil)) Expect(rr.Code).To(Equal(http.StatusNotImplemented)) }) It("calls the backend with the externalId filter", func() { identityEducationBackend.On("FilterEducationSchoolsByAttribute", mock.Anything, "externalId", "ext1234").Return([]*libregraph.EducationSchool{newSchool}, nil) svc.GetEducationSchools(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?$filter="+url.QueryEscape("externalId eq 'ext1234'"), nil)) Expect(rr.Code).To(Equal(http.StatusOK)) }) }) }) Describe("GetEducationSchool", func() { It("handles missing or empty school id", func() { r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil) svc.GetEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", "") r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx)) svc.GetEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) Context("with an existing school", func() { BeforeEach(func() { identityEducationBackend.On("GetEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(newSchool, nil) }) It("gets the school", func() { r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/"+*newSchool.Id, nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx)) svc.GetEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) }) }) }) Describe("PostEducationSchool", func() { It("handles invalid body", func() { r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBufferString("{invalid")) svc.PostEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("handles missing display name", func() { newSchool = libregraph.NewEducationSchool() newSchool.SetSchoolNumber("0034") newSchoolJson, err := json.Marshal(newSchool) Expect(err).ToNot(HaveOccurred()) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson)) svc.PostEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("disallows school create ids", func() { newSchool = libregraph.NewEducationSchool() newSchool.SetId("disallowed") newSchool.SetDisplayName("New School") newSchool.SetSchoolNumber("0034") newSchoolJson, err := json.Marshal(newSchool) Expect(err).ToNot(HaveOccurred()) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson)) svc.PostEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("handles backend errors", func() { newSchool = libregraph.NewEducationSchool() newSchool.SetDisplayName("New School") newSchool.SetSchoolNumber("0034") newSchoolJson, err := json.Marshal(newSchool) Expect(err).ToNot(HaveOccurred()) identityEducationBackend.On("CreateEducationSchool", mock.Anything, mock.Anything).Return(nil, errorcode.New(errorcode.AccessDenied, "access denied")) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson)) svc.PostEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusForbidden)) }) It("creates the school", func() { newSchool = libregraph.NewEducationSchool() newSchool.SetDisplayName("New School") newSchool.SetSchoolNumber("0034") newSchoolJson, err := json.Marshal(newSchool) Expect(err).ToNot(HaveOccurred()) identityEducationBackend.On("CreateEducationSchool", mock.Anything, mock.Anything).Return(newSchool, nil) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson)) svc.PostEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusCreated)) }) }) Describe("updating a School", func() { schoolUpdate := libregraph.NewEducationSchool() schoolUpdate.SetDisplayName("New School Name") schoolUpdateJson, _ := json.Marshal(schoolUpdate) schoolUpdatePast := libregraph.NewEducationSchool() schoolUpdatePast.SetTerminationDate(time.Now().Add(-time.Hour * 1)) schoolUpdatePastJson, _ := json.Marshal(schoolUpdatePast) schoolUpdateBeforeGrace := libregraph.NewEducationSchool() schoolUpdateBeforeGrace.SetTerminationDate(time.Now().Add(24 * 10 * time.Hour)) schoolUpdateBeforeGraceJson, _ := json.Marshal(schoolUpdateBeforeGrace) schoolUpdatePastGrace := libregraph.NewEducationSchool() schoolUpdatePastGrace.SetTerminationDate(time.Now().Add(24 * 31 * time.Hour)) schoolUpdatePastGraceJson, _ := json.Marshal(schoolUpdatePastGrace) BeforeEach(func() { identityEducationBackend.On("UpdateEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(schoolUpdate, nil) }) DescribeTable("PatchEducationSchool", func(schoolId string, body io.Reader, statusCode int) { r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools/", body) rctx := chi.NewRouteContext() if schoolId != "" { rctx.URLParams.Add("schoolID", schoolId) } r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PatchEducationSchool(rr, r) Expect(rr.Code).To(Equal(statusCode)) }, Entry("handles invalid body", "school-id", bytes.NewBufferString("{invalid"), http.StatusBadRequest), Entry("handles missing or empty school id", "", bytes.NewBufferString(""), http.StatusBadRequest), Entry("handles malformed school id", "school%id", bytes.NewBuffer(schoolUpdateJson), http.StatusBadRequest), Entry("updates the school", "school-id", bytes.NewBuffer(schoolUpdateJson), http.StatusOK), Entry("fails to set a termination date in the past", "school-id", bytes.NewBuffer(schoolUpdatePastJson), http.StatusBadRequest), Entry("fails to set a termination date before grace period", "school-id", bytes.NewBuffer(schoolUpdateBeforeGraceJson), http.StatusBadRequest), Entry("succeeds to set a termination date past the grace period", "school-id", bytes.NewBuffer(schoolUpdatePastGraceJson), http.StatusOK), ) }) Describe("DeleteEducationSchool", func() { schoolWithFutureTermination := libregraph.NewEducationSchool() schoolWithFutureTermination.SetId("schoolWithFutureTermination") schoolWithFutureTermination.SetTerminationDate(time.Now().Add(time.Hour * 24)) schoolWithPastTermination := libregraph.NewEducationSchool() schoolWithPastTermination.SetId("schoolWithPastTermination") schoolWithPastTermination.SetTerminationDate(time.Now().Add(-time.Hour * 24)) Context("with an existing school", func() { BeforeEach(func() { identityEducationBackend.On("GetEducationSchool", mock.Anything, "school1").Return(newSchool, nil) identityEducationBackend.On("GetEducationSchool", mock.Anything, "schoolWithFutureTermination", mock.Anything).Return(schoolWithFutureTermination, nil) identityEducationBackend.On("GetEducationSchool", mock.Anything, "schoolWithPastTermination", mock.Anything).Return(schoolWithPastTermination, nil) }) DescribeTable("checks terminnation date", func(schoolId string, statusCode int) { identityEducationBackend.On("DeleteEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil) identityEducationBackend.On("GetEducationSchoolUsers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.EducationUser{}, nil) r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", schoolId) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchool(rr, r) Expect(rr.Code).To(Equal(statusCode)) if rr.Code == http.StatusNoContent { identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "DeleteEducationSchool", 1) } else { identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "DeleteEducationSchool", 0) } }, Entry("fails when school has no termination date", "school1", http.StatusMethodNotAllowed), Entry("fails when school has a termination date in the future", "schoolWithFutureTermination", http.StatusMethodNotAllowed), Entry("succeeds when school has a termination date in the past", "schoolWithPastTermination", http.StatusNoContent), ) It("removes the users from the school", func() { user1 := libregraph.NewEducationUser() user1.SetId("user1") user2 := libregraph.NewEducationUser() user2.SetId("user2") identityEducationBackend.On("GetEducationSchoolUsers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.EducationUser{user1, user2}, nil) identityEducationBackend.On("DeleteEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil) identityEducationBackend.On("RemoveUserFromEducationSchool", mock.Anything, mock.Anything, *user1.Id).Return(nil) identityEducationBackend.On("RemoveUserFromEducationSchool", mock.Anything, mock.Anything, *user2.Id).Return(nil) r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", "schoolWithPastTermination") r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchool(rr, r) Expect(rr.Code).To(Equal(http.StatusNoContent)) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "DeleteEducationSchool", 1) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveUserFromEducationSchool", 2) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "GetEducationSchoolUsers", 1) }) }) }) Describe("GetEducationSchoolUsers", func() { It("gets the list of members", func() { user := libregraph.NewEducationUser() user.SetOnPremisesSamAccountName("user") user.SetDisplayName("display name") user.SetMail("mail") user.SetId("user") identityEducationBackend.On("GetEducationSchoolUsers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.EducationUser{user}, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/{schoolID}/users", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.GetEducationSchoolUsers(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) data, err := io.ReadAll(rr.Body) Expect(err).ToNot(HaveOccurred()) var members []*libregraph.User err = json.Unmarshal(data, &members) Expect(err).ToNot(HaveOccurred()) Expect(len(members)).To(Equal(1)) Expect(members[0].GetId()).To(Equal("user")) }) }) Describe("PostEducationSchoolUsers", func() { It("fails on invalid body", func() { r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBufferString("{invalid")) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolUser(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/schools/{schoolID}/members", bytes.NewBuffer(data)) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolUser(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/schools/{schoolID}/members", bytes.NewBuffer(data)) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolUser(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()) identityEducationBackend.On("AddUsersToEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBuffer(data)) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolUser(rr, r) Expect(rr.Code).To(Equal(http.StatusNoContent)) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddUsersToEducationSchool", 1) }) }) Describe("DeleteEducationSchoolUsers", func() { It("handles missing or empty member id", func() { r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{userID}/$ref", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchoolUser(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/schools/{schoolID}/members/{userID}/$ref", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("userID", "/users/user") r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchoolUser(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("deletes members", func() { identityEducationBackend.On("RemoveUserFromEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil) r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{userID}/$ref", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) rctx.URLParams.Add("userID", "/users/user1") r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchoolUser(rr, r) Expect(rr.Code).To(Equal(http.StatusNoContent)) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveUserFromEducationSchool", 1) }) }) Describe("GetEducationSchoolClasses", func() { It("gets the list of classes", func() { class := libregraph.NewEducationClassWithDefaults() class.SetId("class") identityEducationBackend.On("GetEducationSchoolClasses", mock.Anything, *newSchool.Id).Return([]*libregraph.EducationClass{class}, nil) r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/{schoolID}/classes", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.GetEducationSchoolClasses(rr, r) Expect(rr.Code).To(Equal(http.StatusOK)) data, err := io.ReadAll(rr.Body) Expect(err).ToNot(HaveOccurred()) var classes []*libregraph.EducationClass err = json.Unmarshal(data, &classes) Expect(err).ToNot(HaveOccurred()) Expect(len(classes)).To(Equal(1)) Expect(classes[0].GetId()).To(Equal("class")) }) }) Describe("PostEducationSchoolClasses", func() { It("fails on invalid body", func() { r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/classes", bytes.NewBufferString("{invalid")) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolClass(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/schools/{schoolID}/classes", bytes.NewBuffer(data)) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolClass(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("fails on invalid class refs", func() { class := libregraph.NewMemberReference() class.SetOdataId("/invalidtype/class") data, err := json.Marshal(class) Expect(err).ToNot(HaveOccurred()) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/classesa", bytes.NewBuffer(data)) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolUser(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("adds a new class", func() { member := libregraph.NewMemberReference() member.SetOdataId("/classes/class") data, err := json.Marshal(member) Expect(err).ToNot(HaveOccurred()) identityEducationBackend.On("AddClassesToEducationSchool", mock.Anything, *newSchool.Id, []string{"class"}).Return(nil) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBuffer(data)) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.PostEducationSchoolClass(rr, r) Expect(rr.Code).To(Equal(http.StatusNoContent)) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddClassesToEducationSchool", 1) }) }) Describe("DeleteEducationSchoolClass", func() { It("handles missing or empty member id", func() { r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/classes/{classID}/$ref", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchoolClass(rr, r) Expect(rr.Code).To(Equal(http.StatusBadRequest)) }) It("deletes members", func() { identityEducationBackend.On("RemoveClassFromEducationSchool", mock.Anything, *newSchool.Id, "/classes/class1").Return(nil) r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/classes/{classID}/$ref", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("schoolID", *newSchool.Id) rctx.URLParams.Add("classID", "/classes/class1") r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) svc.DeleteEducationSchoolClass(rr, r) Expect(rr.Code).To(Equal(http.StatusNoContent)) identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveClassFromEducationSchool", 1) }) }) })