package service_test import ( "context" "time" settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0" "github.com/opencloud-eu/reva/v2/pkg/store" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/qsfera/server/pkg/log" "github.com/qsfera/server/pkg/shared" settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0" settingsmocks "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0/mocks" "github.com/qsfera/server/services/graph/pkg/config/defaults" "github.com/qsfera/server/services/notifications/pkg/channels" "github.com/qsfera/server/services/notifications/pkg/service" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/reva/v2/pkg/utils" cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" "github.com/stretchr/testify/mock" "google.golang.org/grpc" ) var _ = Describe("Notifications", func() { var ( gatewayClient *cs3mocks.GatewayAPIClient gatewaySelector pool.Selectable[gateway.GatewayAPIClient] vs *settingsmocks.ValueService sharer = &user.User{ Id: &user.UserId{ OpaqueId: "sharer", }, Mail: "sharer@qsfera.eu", DisplayName: "Dr. S. Harer", } sharee = &user.User{ Id: &user.UserId{ OpaqueId: "sharee", }, Mail: "sharee@qsfera.eu", DisplayName: "Eric Expireling", } resourceid = &provider.ResourceId{ StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "itemid", } ) 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 }, ) gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once() gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once() gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil) gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Info: &provider.ResourceInfo{Name: "secrets of the board", Space: &provider.StorageSpace{Name: "secret space"}}}, nil) vs = &settingsmocks.ValueService{} vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(&settingssvc.GetValueResponse{ Value: &settingsmsg.ValueWithIdentifier{ Value: &settingsmsg.Value{ Value: &settingsmsg.Value_CollectionValue{ CollectionValue: &settingsmsg.CollectionValue{ Values: []*settingsmsg.CollectionOption{ { Key: "mail", Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: true}, }, }, }, }, }, }, }, nil) }) DescribeTable("Sending userEventIds", func(tc testChannel, ev events.Event) { cfg := defaults.FullDefaultConfig() cfg.GRPCClientTLS = &shared.GRPCClientTLS{} ch := make(chan events.Event) evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "", "", "", "", store.Create(), nil, nil) go evts.Run() ch <- ev select { case <-tc.done: // finished case <-time.Tick(3 * time.Second): Fail("timeout waiting for notification") } }, Entry("Share Created", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer shared 'secrets of the board' with you", expectedTextBody: `Hello Eric Expireling Dr. S. Harer has shared "secrets of the board" with you. Click here to view it: files/shares/with-me --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedSender: sharer.GetDisplayName(), done: make(chan struct{}), }, events.Event{ Event: events.ShareCreated{ Sharer: sharer.GetId(), GranteeUserID: sharee.GetId(), CTime: utils.TimeToTS(time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC)), ItemID: resourceid, }, }), Entry("Share Expired", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Share to 'secrets of the board' expired at 2023-04-17 16:42:00", expectedTextBody: `Hello Eric Expireling, Your share to secrets of the board has expired at 2023-04-17 16:42:00 Even though this share has been revoked you still might have access through other shares and/or space memberships. --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedSender: sharer.GetDisplayName(), done: make(chan struct{}), }, events.Event{ Event: events.ShareExpired{ ShareOwner: sharer.GetId(), GranteeUserID: sharee.GetId(), ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC), ItemID: resourceid, }, }), Entry("Added to Space", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer invited you to join secret space", expectedTextBody: `Hello Eric Expireling, Dr. S. Harer has invited you to join "secret space". Click here to view it: f/spaceid --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedSender: sharer.GetDisplayName(), done: make(chan struct{}), }, events.Event{ Event: events.SpaceShared{ Executant: sharer.GetId(), Creator: sharer.GetId(), GranteeUserID: sharee.GetId(), ID: &provider.StorageSpaceId{OpaqueId: "spaceid"}, }, }), Entry("Removed from Space", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer removed you from secret space", expectedTextBody: `Hello Eric Expireling, Dr. S. Harer has removed you from "secret space". You might still have access through your other groups or direct membership. Click here to check it: f/spaceid --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedSender: sharer.GetDisplayName(), done: make(chan struct{}), }, events.Event{ Event: events.SpaceUnshared{ Executant: sharer.GetId(), GranteeUserID: sharee.GetId(), ID: &provider.StorageSpaceId{OpaqueId: "spaceid"}, }, }), Entry("Space Expired", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Membership of 'secret space' expired at 2023-04-17 16:42:00", expectedTextBody: `Hello Eric Expireling, Your membership of space secret space has expired at 2023-04-17 16:42:00 Even though this membership has expired you still might have access through other shares and/or space memberships --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedSender: sharer.GetDisplayName(), done: make(chan struct{}), }, events.Event{ Event: events.SpaceMembershipExpired{ SpaceOwner: sharer.GetId(), GranteeUserID: sharee.GetId(), SpaceID: &provider.StorageSpaceId{OpaqueId: "spaceid"}, SpaceName: "secret space", ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC), }, }), ) }) var _ = Describe("Notifications X-Site Scripting", func() { var ( gatewayClient *cs3mocks.GatewayAPIClient gatewaySelector pool.Selectable[gateway.GatewayAPIClient] vs *settingsmocks.ValueService sharer = &user.User{ Id: &user.UserId{ OpaqueId: "sharer", }, Mail: "sharer@qsfera.eu", DisplayName: "Dr. O'reilly", } sharee = &user.User{ Id: &user.UserId{ OpaqueId: "sharee", }, Mail: "sharee@qsfera.eu", DisplayName: "", } resourceid = &provider.ResourceId{ StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "itemid", } ) 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 }, ) gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once() gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once() gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil) gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{ Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Info: &provider.ResourceInfo{ Name: "", Space: &provider.StorageSpace{Name: ""}}, }, nil) vs = &settingsmocks.ValueService{} vs.On("GetValueByUniqueIdentifiers", mock.Anything, mock.Anything).Return(&settingssvc.GetValueResponse{ Value: &settingsmsg.ValueWithIdentifier{ Value: &settingsmsg.Value{ Value: &settingsmsg.Value_CollectionValue{ CollectionValue: &settingsmsg.CollectionValue{ Values: []*settingsmsg.CollectionOption{ { Key: "mail", Option: &settingsmsg.CollectionOption_BoolValue{BoolValue: true}, }, }, }, }, }, }, }, nil) }) DescribeTable("Sending userEventIds", func(tc testChannel, ev events.Event) { cfg := defaults.FullDefaultConfig() cfg.GRPCClientTLS = &shared.GRPCClientTLS{} ch := make(chan events.Event) evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "", "", "", "", store.Create(), nil, nil) go evts.Run() ch <- ev select { case <-tc.done: // finished case <-time.Tick(3 * time.Second): Fail("timeout waiting for notification") } }, Entry("Share Created", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. O'reilly shared '' with you", expectedTextBody: `Hello Dr. O'reilly has shared "" with you. Click here to view it: files/shares/with-me --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedHTMLBody: `
  Hello <script>alert('Eric Expireling');</script>

Dr. O'reilly has shared "<script>alert('secrets of the board');</script>" with you.

Click here to view it: files/shares/with-me
 
 
 
`, expectedSender: sharer.GetDisplayName(), done: make(chan struct{}), }, events.Event{ Event: events.ShareCreated{ Sharer: sharer.GetId(), GranteeUserID: sharee.GetId(), CTime: utils.TimeToTS(time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC)), ItemID: resourceid, }, }), Entry("Added to Space", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. O'reilly invited you to join ", expectedTextBody: `Hello , Dr. O'reilly has invited you to join "". Click here to view it: f/spaceid --- КуСфера - a safe home for all your data https://qsfera.eu `, expectedSender: sharer.GetDisplayName(), expectedHTMLBody: `
  Hello <script>alert('Eric Expireling');</script>,

Dr. O'reilly has invited you to join "<script>alert('secret space');</script>".

Click here to view it: f/spaceid
 
 
 
`, done: make(chan struct{}), }, events.Event{ Event: events.SpaceShared{ Executant: sharer.GetId(), Creator: sharer.GetId(), GranteeUserID: sharee.GetId(), ID: &provider.StorageSpaceId{OpaqueId: "spaceid"}, }, }), ) }) // NOTE: This is explictitly not testing the message itself. Should we? type testChannel struct { expectedReceipients []string expectedSubject string expectedTextBody string expectedHTMLBody string expectedSender string done chan struct{} } func (tc testChannel) SendMessage(ctx context.Context, m *channels.Message) error { defer GinkgoRecover() Expect(tc.expectedReceipients).To(Equal(m.Recipient)) Expect(tc.expectedSubject).To(Equal(m.Subject)) Expect(tc.expectedTextBody).To(Equal(m.TextBody)) Expect(tc.expectedSender).To(Equal(m.Sender)) if tc.expectedHTMLBody != "" { Expect(tc.expectedHTMLBody).To(Equal(m.HTMLBody)) } tc.done <- struct{}{} return nil }