diff --git a/services/graph/Makefile b/services/graph/Makefile index 070214eac3..692f9ede17 100644 --- a/services/graph/Makefile +++ b/services/graph/Makefile @@ -29,6 +29,7 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this $(MOCKERY) --dir pkg/service/v0 --case underscore --name HTTPClient $(MOCKERY) --dir pkg/service/v0 --case underscore --name Publisher $(MOCKERY) --dir pkg/service/v0 --case underscore --name Permissions + $(MOCKERY) --dir pkg/service/v0 --case underscore --name RoleService $(MOCKERY) --dir pkg/identity --output pkg/identity/mocks --case underscore --name Backend $(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client diff --git a/services/graph/mocks/permissions.go b/services/graph/mocks/permissions.go index 78b4704705..614d779ae3 100644 --- a/services/graph/mocks/permissions.go +++ b/services/graph/mocks/permissions.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.4. DO NOT EDIT. +// Code generated by mockery v2.14.1. DO NOT EDIT. package mocks @@ -76,3 +76,18 @@ func (_m *Permissions) ListPermissionsByResource(ctx context.Context, in *v0.Lis return r0, r1 } + +type mockConstructorTestingTNewPermissions interface { + mock.TestingT + Cleanup(func()) +} + +// NewPermissions creates a new instance of Permissions. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewPermissions(t mockConstructorTestingTNewPermissions) *Permissions { + mock := &Permissions{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/mocks/role_service.go b/services/graph/mocks/role_service.go new file mode 100644 index 0000000000..b2d3063dbb --- /dev/null +++ b/services/graph/mocks/role_service.go @@ -0,0 +1,155 @@ +// Code generated by mockery v2.14.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "go-micro.dev/v4/client" + + emptypb "google.golang.org/protobuf/types/known/emptypb" + + mock "github.com/stretchr/testify/mock" + + v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" +) + +// RoleService is an autogenerated mock type for the RoleService type +type RoleService struct { + mock.Mock +} + +// AssignRoleToUser provides a mock function with given fields: ctx, in, opts +func (_m *RoleService) AssignRoleToUser(ctx context.Context, in *v0.AssignRoleToUserRequest, opts ...client.CallOption) (*v0.AssignRoleToUserResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *v0.AssignRoleToUserResponse + if rf, ok := ret.Get(0).(func(context.Context, *v0.AssignRoleToUserRequest, ...client.CallOption) *v0.AssignRoleToUserResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.AssignRoleToUserResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *v0.AssignRoleToUserRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRoleAssignments provides a mock function with given fields: ctx, in, opts +func (_m *RoleService) ListRoleAssignments(ctx context.Context, in *v0.ListRoleAssignmentsRequest, opts ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *v0.ListRoleAssignmentsResponse + if rf, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsRequest, ...client.CallOption) *v0.ListRoleAssignmentsResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.ListRoleAssignmentsResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *v0.ListRoleAssignmentsRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRoles provides a mock function with given fields: ctx, in, opts +func (_m *RoleService) ListRoles(ctx context.Context, in *v0.ListBundlesRequest, opts ...client.CallOption) (*v0.ListBundlesResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *v0.ListBundlesResponse + if rf, ok := ret.Get(0).(func(context.Context, *v0.ListBundlesRequest, ...client.CallOption) *v0.ListBundlesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0.ListBundlesResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *v0.ListBundlesRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveRoleFromUser provides a mock function with given fields: ctx, in, opts +func (_m *RoleService) RemoveRoleFromUser(ctx context.Context, in *v0.RemoveRoleFromUserRequest, opts ...client.CallOption) (*emptypb.Empty, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *emptypb.Empty + if rf, ok := ret.Get(0).(func(context.Context, *v0.RemoveRoleFromUserRequest, ...client.CallOption) *emptypb.Empty); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*emptypb.Empty) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *v0.RemoveRoleFromUserRequest, ...client.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewRoleService interface { + mock.TestingT + Cleanup(func()) +} + +// NewRoleService creates a new instance of RoleService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRoleService(t mockConstructorTestingTNewRoleService) *RoleService { + mock := &RoleService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go index f1aa0d140c..ca218a128a 100644 --- a/services/graph/pkg/service/v0/graph.go +++ b/services/graph/pkg/service/v0/graph.go @@ -18,6 +18,7 @@ import ( "go-micro.dev/v4/client" mevents "go-micro.dev/v4/events" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" ) //go:generate make -C ../../.. generate @@ -77,6 +78,14 @@ type HTTPClient interface { // GetGatewayServiceClientFunc is a callback used to pass in a mock during testing type GetGatewayServiceClientFunc func() (GatewayClient, error) +// RoleService is the interface used to access the role service +type RoleService interface { + ListRoles(ctx context.Context, in *settingssvc.ListBundlesRequest, opts ...client.CallOption) (*settingssvc.ListBundlesResponse, error) + ListRoleAssignments(ctx context.Context, in *settingssvc.ListRoleAssignmentsRequest, opts ...client.CallOption) (*settingssvc.ListRoleAssignmentsResponse, error) + AssignRoleToUser(ctx context.Context, in *settingssvc.AssignRoleToUserRequest, opts ...client.CallOption) (*settingssvc.AssignRoleToUserResponse, error) + RemoveRoleFromUser(ctx context.Context, in *settingssvc.RemoveRoleFromUserRequest, opts ...client.CallOption) (*emptypb.Empty, error) +} + // Graph defines implements the business logic for Service. type Graph struct { config *config.Config @@ -84,7 +93,7 @@ type Graph struct { logger *log.Logger identityBackend identity.Backend gatewayClient GatewayClient - roleService settingssvc.RoleService + roleService RoleService permissionsService Permissions spacePropertiesCache *ttlcache.Cache eventsPublisher events.Publisher diff --git a/services/graph/pkg/service/v0/option.go b/services/graph/pkg/service/v0/option.go index dcaa01a422..a9a70b5bbd 100644 --- a/services/graph/pkg/service/v0/option.go +++ b/services/graph/pkg/service/v0/option.go @@ -21,7 +21,7 @@ type Options struct { Middleware []func(http.Handler) http.Handler GatewayClient GatewayClient IdentityBackend identity.Backend - RoleService settingssvc.RoleService + RoleService RoleService PermissionService Permissions RoleManager *roles.Manager EventsPublisher events.Publisher @@ -73,8 +73,8 @@ func WithIdentityBackend(val identity.Backend) Option { } } -// RoleService provides a function to set the RoleService option. -func RoleService(val settingssvc.RoleService) Option { +// WithRoleService provides a function to set the RoleService option. +func WithRoleService(val RoleService) Option { return func(o *Options) { o.RoleService = val } diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index cb0e05f26b..ca1d717f8e 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -257,6 +257,7 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) { d, err := g.cs3StorageSpaceToDrive(r.Context(), wdu, sp) if err != nil { logger.Debug().Err(err).Interface("id", sp.Id).Msg("error converting space to drive") + continue } quota, err := g.getDriveQuota(r.Context(), sp) if err != nil { diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go new file mode 100644 index 0000000000..c09f8e4d76 --- /dev/null +++ b/services/graph/pkg/service/v0/users_test.go @@ -0,0 +1,529 @@ +package svc_test + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/go-chi/chi/v5" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + + libregraph "github.com/owncloud/libre-graph-api-go" + ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" + "github.com/owncloud/ocis/v2/ocis-pkg/shared" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "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" +) + +type userList struct { + Value []*libregraph.User +} + +var _ = Describe("Users", func() { + var ( + svc service.Service + ctx context.Context + cfg *config.Config + gatewayClient *mocks.GatewayClient + eventsPublisher mocks.Publisher + roleService *mocks.RoleService + identityBackend *identitymocks.Backend + + rr *httptest.ResponseRecorder + + currentUser = &userv1beta1.User{ + Id: &userv1beta1.UserId{ + OpaqueId: "user", + }, + } + ) + + BeforeEach(func() { + eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + identityBackend = &identitymocks.Backend{} + roleService = &mocks.RoleService{} + gatewayClient = &mocks.GatewayClient{} + + 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.WithRoleService(roleService), + ) + }) + + Describe("GetMe", func() { + It("handles missing user", func() { + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me", nil) + svc.GetMe(rr, r) + + Expect(rr.Code).To(Equal(http.StatusInternalServerError)) + }) + + It("gets the information", func() { + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me", nil) + r = r.WithContext(ctxpkg.ContextSetUser(ctx, currentUser)) + svc.GetMe(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + }) + + It("expands the user", func() { + user := &libregraph.User{} + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me?$expand=memberOf", nil) + r = r.WithContext(ctxpkg.ContextSetUser(ctx, currentUser)) + svc.GetMe(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + }) + }) + + Describe("GetUsers", func() { + It("handles invalid requests", func() { + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users?$invalid=true", nil) + svc.GetUsers(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("lists the users", func() { + user := &libregraph.User{} + user.SetId("user1") + users := []*libregraph.User{user} + + identityBackend.On("GetUsers", mock.Anything, mock.Anything, mock.Anything).Return(users, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users", nil) + svc.GetUsers(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := ioutil.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + res := userList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(res.Value)).To(Equal(1)) + Expect(res.Value[0].GetId()).To(Equal("user1")) + }) + + It("sorts", func() { + user := &libregraph.User{} + user.SetId("user1") + user.SetMail("z@example.com") + user.SetDisplayName("9") + user.SetOnPremisesSamAccountName("9") + user2 := &libregraph.User{} + user2.SetId("user2") + user2.SetMail("a@example.com") + user2.SetDisplayName("1") + user2.SetOnPremisesSamAccountName("1") + users := []*libregraph.User{user, user2} + + identityBackend.On("GetUsers", mock.Anything, mock.Anything, mock.Anything).Return(users, nil) + + getUsers := func(path string) []*libregraph.User { + r := httptest.NewRequest(http.MethodGet, path, nil) + rec := httptest.NewRecorder() + svc.GetUsers(rec, r) + + Expect(rec.Code).To(Equal(http.StatusOK)) + data, err := ioutil.ReadAll(rec.Body) + Expect(err).ToNot(HaveOccurred()) + + res := userList{} + err = json.Unmarshal(data, &res) + Expect(err).ToNot(HaveOccurred()) + return res.Value + } + + unsorted := getUsers("/graph/v1.0/me/users") + Expect(len(unsorted)).To(Equal(2)) + Expect(unsorted[0].GetId()).To(Equal("user1")) + Expect(unsorted[1].GetId()).To(Equal("user2")) + + byMail := getUsers("/graph/v1.0/me/users?$orderby=mail") + Expect(len(byMail)).To(Equal(2)) + Expect(byMail[0].GetId()).To(Equal("user2")) + Expect(byMail[1].GetId()).To(Equal("user1")) + byMail = getUsers("/graph/v1.0/me/users?$orderby=mail%20asc") + Expect(len(byMail)).To(Equal(2)) + Expect(byMail[0].GetId()).To(Equal("user2")) + Expect(byMail[1].GetId()).To(Equal("user1")) + byMail = getUsers("/graph/v1.0/me/users?$orderby=mail%20desc") + Expect(len(byMail)).To(Equal(2)) + Expect(byMail[0].GetId()).To(Equal("user1")) + Expect(byMail[1].GetId()).To(Equal("user2")) + + byDisplayName := getUsers("/graph/v1.0/me/users?$orderby=displayName") + Expect(len(byDisplayName)).To(Equal(2)) + Expect(byDisplayName[0].GetId()).To(Equal("user2")) + Expect(byDisplayName[1].GetId()).To(Equal("user1")) + byDisplayName = getUsers("/graph/v1.0/me/users?$orderby=displayName%20asc") + Expect(len(byDisplayName)).To(Equal(2)) + Expect(byDisplayName[0].GetId()).To(Equal("user2")) + Expect(byDisplayName[1].GetId()).To(Equal("user1")) + byDisplayName = getUsers("/graph/v1.0/me/users?$orderby=displayName%20desc") + Expect(len(byDisplayName)).To(Equal(2)) + Expect(byDisplayName[0].GetId()).To(Equal("user1")) + Expect(byDisplayName[1].GetId()).To(Equal("user2")) + + byOnPremisesSamAccountName := getUsers("/graph/v1.0/me/users?$orderby=onPremisesSamAccountName") + Expect(len(byOnPremisesSamAccountName)).To(Equal(2)) + Expect(byOnPremisesSamAccountName[0].GetId()).To(Equal("user2")) + Expect(byOnPremisesSamAccountName[1].GetId()).To(Equal("user1")) + byOnPremisesSamAccountName = getUsers("/graph/v1.0/me/users?$orderby=onPremisesSamAccountName%20asc") + Expect(len(byOnPremisesSamAccountName)).To(Equal(2)) + Expect(byOnPremisesSamAccountName[0].GetId()).To(Equal("user2")) + Expect(byOnPremisesSamAccountName[1].GetId()).To(Equal("user1")) + byOnPremisesSamAccountName = getUsers("/graph/v1.0/me/users?$orderby=onPremisesSamAccountName%20desc") + Expect(len(byOnPremisesSamAccountName)).To(Equal(2)) + Expect(byOnPremisesSamAccountName[0].GetId()).To(Equal("user1")) + Expect(byOnPremisesSamAccountName[1].GetId()).To(Equal("user2")) + + // Handles invalid sort field + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users?$orderby=invalid", nil) + svc.GetUsers(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + }) + + Describe("GetUser", func() { + It("handles missing userids", func() { + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users", nil) + svc.GetUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("gets the user", func() { + user := &libregraph.User{} + user.SetId("user1") + + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", *user.Id) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.GetUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := ioutil.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + responseUser := &libregraph.User{} + err = json.Unmarshal(data, &responseUser) + Expect(err).ToNot(HaveOccurred()) + Expect(responseUser.GetId()).To(Equal("user1")) + Expect(len(responseUser.GetDrives())).To(Equal(0)) + }) + + It("includes the personal space if requested", func() { + user := &libregraph.User{} + user.SetId("user1") + + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) + gatewayClient.On("GetQuota", mock.Anything, mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{ + Status: status.NewOK(ctx), + TotalBytes: 10, + }, nil) + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*provider.StorageSpace{ + { + Id: &provider.StorageSpaceId{OpaqueId: "drive1"}, + Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"}, + SpaceType: "project", + }, + { + Id: &provider.StorageSpaceId{OpaqueId: "personal"}, + Root: &provider.ResourceId{SpaceId: "personal", OpaqueId: "personal"}, + SpaceType: "personal", + }, + }, + }, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users?$expand=drive", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", *user.Id) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.GetUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := ioutil.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + responseUser := &libregraph.User{} + err = json.Unmarshal(data, &responseUser) + Expect(err).ToNot(HaveOccurred()) + Expect(responseUser.GetId()).To(Equal("user1")) + Expect(*responseUser.GetDrive().Id).To(Equal("personal")) + }) + + It("includes the drives if requested", func() { + user := &libregraph.User{} + user.SetId("user1") + + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil) + gatewayClient.On("GetQuota", mock.Anything, mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{ + Status: status.NewOK(ctx), + TotalBytes: 10, + }, nil) + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*provider.StorageSpace{ + { + Id: &provider.StorageSpaceId{OpaqueId: "drive1"}, + Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"}, + }, + }, + }, nil) + + r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/users?$expand=drives", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", *user.Id) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.GetUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err := ioutil.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + responseUser := &libregraph.User{} + err = json.Unmarshal(data, &responseUser) + Expect(err).ToNot(HaveOccurred()) + Expect(responseUser.GetId()).To(Equal("user1")) + Expect(len(responseUser.GetDrives())).To(Equal(1)) + }) + }) + + Describe("PostUser", func() { + var ( + user *libregraph.User + + assertHandleBadAttributes = func(user *libregraph.User) { + userJson, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users", bytes.NewBuffer(userJson)) + svc.PostUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + } + ) + + BeforeEach(func() { + user = &libregraph.User{} + user.SetDisplayName("Display Name") + user.SetOnPremisesSamAccountName("user") + user.SetMail("user@example.com") + }) + + It("handles invalid bodies", func() { + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users?$invalid=true", nil) + svc.PostUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("handles missing display names", func() { + user.DisplayName = nil + assertHandleBadAttributes(user) + + }) + + It("handles missing OnPremisesSamAccountName", func() { + user.OnPremisesSamAccountName = nil + assertHandleBadAttributes(user) + + user.SetOnPremisesSamAccountName("") + assertHandleBadAttributes(user) + }) + + It("handles bad Mails", func() { + user.Mail = nil + assertHandleBadAttributes(user) + + user.SetMail("not-a-mail-address") + assertHandleBadAttributes(user) + }) + + It("handles set Ids - they are read-only", func() { + user.SetId("/users/user") + assertHandleBadAttributes(user) + }) + + It("creates a user", func() { + roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settingssvc.AssignRoleToUserResponse{}, nil) + identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User { + user.SetId("/users/user") + return &user + }, nil) + userJson, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users", bytes.NewBuffer(userJson)) + r = r.WithContext(ctxpkg.ContextSetUser(ctx, currentUser)) + svc.PostUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + }) + }) + + Describe("DeleteUser", func() { + It("handles missing userids", func() { + r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/me/users/{userid}", nil) + svc.DeleteUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("prevents a user from deleting themselves", func() { + lu := libregraph.User{} + lu.SetId(currentUser.Id.OpaqueId) + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(&lu, nil) + + r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/me/users/{userid}", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", currentUser.Id.OpaqueId) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.DeleteUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusForbidden)) + }) + + It("deletes a user from deleting themselves", func() { + otheruser := &userv1beta1.User{ + Id: &userv1beta1.UserId{ + OpaqueId: "otheruser", + }, + } + + lu := libregraph.User{} + lu.SetId(otheruser.Id.OpaqueId) + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(&lu, nil) + identityBackend.On("DeleteUser", mock.Anything, mock.Anything).Return(nil) + gatewayClient.On("DeleteStorageSpace", mock.Anything, mock.Anything).Return(&provider.DeleteStorageSpaceResponse{ + Status: status.NewOK(ctx), + }, nil) + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*provider.StorageSpace{ + { + Opaque: &typesv1beta1.Opaque{}, + Id: &provider.StorageSpaceId{OpaqueId: "drive1"}, + Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"}, + SpaceType: "personal", + Owner: otheruser, + }, + }, + }, nil) + + r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/me/users/{userid}", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", lu.GetId()) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.DeleteUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusNoContent)) + gatewayClient.AssertNumberOfCalls(GinkgoT(), "DeleteStorageSpace", 2) // 2 calls for the home space. first trash, then purge + }) + }) + + Describe("PatchUser", func() { + var ( + user *libregraph.User + ) + + BeforeEach(func() { + user = &libregraph.User{} + user.SetDisplayName("Display Name") + user.SetOnPremisesSamAccountName("user") + user.SetMail("user@example.com") + user.SetId("/users/user") + + identityBackend.On("GetUser", mock.Anything, mock.Anything, mock.Anything).Return(&user, nil) + }) + + It("handles missing userids", func() { + r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/users/{userid}", nil) + svc.PatchUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("handles invalid bodies", func() { + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users?$invalid=true", nil) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.PatchUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("handles invalid email", func() { + user.SetMail("invalid") + data, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users?$invalid=true", bytes.NewBuffer(data)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.PatchUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusBadRequest)) + }) + + It("updates attributes", func() { + identityBackend.On("UpdateUser", mock.Anything, user.GetId(), mock.Anything).Return(user, nil) + + user.SetDisplayName("New Display Name") + data, err := json.Marshal(user) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users?$invalid=true", bytes.NewBuffer(data)) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("userID", user.GetId()) + r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx)) + svc.PatchUser(rr, r) + + Expect(rr.Code).To(Equal(http.StatusOK)) + data, err = ioutil.ReadAll(rr.Body) + Expect(err).ToNot(HaveOccurred()) + + updatedUser := libregraph.User{} + err = json.Unmarshal(data, &updatedUser) + Expect(err).ToNot(HaveOccurred()) + Expect(updatedUser.GetDisplayName()).To(Equal("New Display Name")) + }) + }) +})