Repository: owncloud/ocis (go)

Issue:
Search works incorrect after MOVE request
ocis local: 
branch: v2.0.0-alpha1


Preparation: 
- rm -rf /.ocis
- run ocis
- admin creates New folder
- inside the "New folder" folder create: folder "test" and file "test.txt"

Steps: 
- in the home page try to search by "test" - work correct
![Screenshot 2022-05-05 at 16 03 47](https://user-images.githubusercontent.com/84779829/166940900-f50794c2-7341-4d67-b853-1eae8225c800.png)

- try to send request "MOVE" (rename or remove to trash) file "test.txt" or folder "test"
- in the home page try again to search by "test" - 
![Screenshot 2022-05-05 at 16 08 53](https://user-images.githubusercontent.com/84779829/166942593-56e75dab-663f-4bd7-a23e-2f9367a532eb.png)

Actual: after renaming file, search finds it in "personal", but file is in a different location

Pull request description:
More fixes to searching in shares
This PR fixes a problem when searching in sharey by making the search results relative to the mountpoint space instead of the grant space.
It also adds more debug logs and changes the search to still continue and return whatever it can find in case there's a problem searching within one space.

Fixes #3701
Fixes #3700 

Patch:
diff --git a/extensions/search/pkg/search/index/index.go b/extensions/search/pkg/search/index/index.go
index 0c2ffecfd9f..bc06ba4665c 100644
--- a/extensions/search/pkg/search/index/index.go
+++ b/extensions/search/pkg/search/index/index.go
@@ -26,7 +26,10 @@ import (
 	"time"
 
 	"github.com/blevesearch/bleve/v2"
+	"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
 	"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
+	"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
+	"github.com/blevesearch/bleve/v2/analysis/tokenizer/single"
 	"github.com/blevesearch/bleve/v2/mapping"
 	"google.golang.org/protobuf/types/known/timestamppb"
 
@@ -57,7 +60,11 @@ type Index struct {
 
 // NewPersisted returns a new instance of Index with the data being persisted in the given directory
 func NewPersisted(path string) (*Index, error) {
-	bi, err := bleve.New(path, BuildMapping())
+	mapping, err := BuildMapping()
+	if err != nil {
+		return nil, err
+	}
+	bi, err := bleve.New(path, mapping)
 	if err != nil {
 		return nil, err
 	}
@@ -157,14 +164,14 @@ func (i *Index) Purge(id *sprovider.ResourceId) error {
 	return i.bleveIndex.Delete(idToBleveId(id))
 }
 
-// Purge removes an entity from the index
-func (i *Index) Move(ri *sprovider.ResourceInfo) error {
+// Move update the path of an entry and all its children
+func (i *Index) Move(ri *sprovider.ResourceInfo, fullPath string) error {
 	doc, err := i.getEntity(idToBleveId(ri.Id))
 	if err != nil {
 		return err
 	}
 	oldName := doc.Path
-	newName := utils.MakeRelativePath(ri.Path)
+	newName := utils.MakeRelativePath(fullPath)
 
 	doc, err = i.updateEntity(idToBleveId(ri.Id), func(doc *indexDocument) {
 		doc.Path = newName
@@ -204,7 +211,7 @@ func (i *Index) Search(ctx context.Context, req *searchsvc.SearchIndexRequest) (
 	deletedQuery := bleve.NewBoolFieldQuery(false)
 	deletedQuery.SetField("Deleted")
 	query := bleve.NewConjunctionQuery(
-		bleve.NewQueryStringQuery("Name:"+req.Query),
+		bleve.NewQueryStringQuery("Name:"+strings.ToLower(req.Query)),
 		deletedQuery, // Skip documents that have been marked as deleted
 		bleve.NewQueryStringQuery("RootID:"+req.Ref.ResourceId.StorageId+"!"+req.Ref.ResourceId.OpaqueId), // Limit search to the space
 		bleve.NewQueryStringQuery("Path:"+utils.MakeRelativePath(path.Join(req.Ref.Path, "/"))+"*"),       // Limit search to this directory in the space
@@ -232,10 +239,29 @@ func (i *Index) Search(ctx context.Context, req *searchsvc.SearchIndexRequest) (
 }
 
 // BuildMapping builds a bleve index mapping which can be used for indexing
-func BuildMapping() mapping.IndexMapping {
+func BuildMapping() (mapping.IndexMapping, error) {
+	nameMapping := bleve.NewTextFieldMapping()
+	nameMapping.Analyzer = "lowercaseKeyword"
+
+	docMapping := bleve.NewDocumentMapping()
+	docMapping.AddFieldMappingsAt("Name", nameMapping)
+
 	indexMapping := bleve.NewIndexMapping()
 	indexMapping.DefaultAnalyzer = keyword.Name
-	return indexMapping
+	indexMapping.DefaultMapping = docMapping
+	err := indexMapping.AddCustomAnalyzer("lowercaseKeyword",
+		map[string]interface{}{
+			"type":      custom.Name,
+			"tokenizer": single.Name,
+			"token_filters": []string{
+				lowercase.Name,
+			},
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	return indexMapping, nil
 }
 
 func toEntity(ref *sprovider.Reference, ri *sprovider.ResourceInfo) *indexDocument {
diff --git a/extensions/search/pkg/search/mocks/IndexClient.go b/extensions/search/pkg/search/mocks/IndexClient.go
index ea79cd50648..f43f9f3a986 100644
--- a/extensions/search/pkg/search/mocks/IndexClient.go
+++ b/extensions/search/pkg/search/mocks/IndexClient.go
@@ -65,13 +65,13 @@ func (_m *IndexClient) DocCount() (uint64, error) {
 	return r0, r1
 }
 
-// Move provides a mock function with given fields: ri
-func (_m *IndexClient) Move(ri *providerv1beta1.ResourceInfo) error {
-	ret := _m.Called(ri)
+// Move provides a mock function with given fields: ri, path
+func (_m *IndexClient) Move(ri *providerv1beta1.ResourceInfo, path string) error {
+	ret := _m.Called(ri, path)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceInfo) error); ok {
-		r0 = rf(ri)
+	if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceInfo, string) error); ok {
+		r0 = rf(ri, path)
 	} else {
 		r0 = ret.Error(0)
 	}
diff --git a/extensions/search/pkg/search/provider/events.go b/extensions/search/pkg/search/provider/events.go
index 90606760706..d243c523778 100644
--- a/extensions/search/pkg/search/provider/events.go
+++ b/extensions/search/pkg/search/provider/events.go
@@ -6,8 +6,10 @@ import (
 	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"
+	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
+	"github.com/cs3org/reva/v2/pkg/errtypes"
 	"github.com/cs3org/reva/v2/pkg/events"
 	"google.golang.org/grpc/metadata"
 )
@@ -59,17 +61,25 @@ func (p *Provider) handleEvent(ev interface{}) {
 			p.logger.Error().Err(err).Msg("failed to stat the changed resource")
 			return
 		}
-
-		switch statRes.Status.Code {
-		case rpc.Code_CODE_OK:
-			err = p.indexClient.Move(statRes.Info)
-			if err != nil {
-				p.logger.Error().Err(err).Msg("failed to restore the changed resource in the index")
-			}
-		default:
+		if statRes.Status.Code != rpc.Code_CODE_OK {
 			p.logger.Error().Interface("statRes", statRes).Msg("failed to stat the changed resource")
+			return
 		}
 
+		gpRes, err := p.getPath(statRes.Info.Id, owner)
+		if err != nil {
+			p.logger.Error().Err(err).Interface("ref", ref).Msg("failed to get path for moved resource")
+			return
+		}
+		if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
+			p.logger.Error().Interface("status", gpRes.Status).Interface("ref", ref).Msg("failed to get path for moved resource")
+			return
+		}
+
+		err = p.indexClient.Move(statRes.Info, gpRes.Path)
+		if err != nil {
+			p.logger.Error().Err(err).Msg("failed to restore the changed resource in the index")
+		}
 		return
 	case events.ContainerCreated:
 		ref = e.Ref
@@ -111,18 +121,38 @@ func (p *Provider) handleEvent(ev interface{}) {
 }
 
 func (p *Provider) statResource(ref *provider.Reference, owner *user.User) (*provider.StatResponse, error) {
-	// Get auth
+	ownerCtx, err := p.getAuthContext(owner)
+	if err != nil {
+		return nil, err
+	}
+
+	// Stat changed resource resource
+	return p.gwClient.Stat(ownerCtx, &provider.StatRequest{Ref: ref})
+}
+
+func (p *Provider) getPath(id *provider.ResourceId, owner *user.User) (*provider.GetPathResponse, error) {
+	ownerCtx, err := p.getAuthContext(owner)
+	if err != nil {
+		return nil, err
+	}
+
+	// Stat changed resource resource
+	return p.gwClient.GetPath(ownerCtx, &provider.GetPathRequest{ResourceId: id})
+}
+
+func (p *Provider) getAuthContext(owner *user.User) (context.Context, error) {
 	ownerCtx := ctxpkg.ContextSetUser(context.Background(), owner)
 	authRes, err := p.gwClient.Authenticate(ownerCtx, &gateway.AuthenticateRequest{
 		Type:         "machine",
 		ClientId:     "userid:" + owner.Id.OpaqueId,
 		ClientSecret: p.machineAuthAPIKey,
 	})
-	if err != nil || authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
+	if err == nil && authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
+		err = errtypes.NewErrtypeFromStatus(authRes.Status)
+	}
+	if err != nil {
 		p.logger.Error().Err(err).Interface("authRes", authRes).Msg("error using machine auth")
+		return nil, err
 	}
-	ownerCtx = metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token)
-
-	// Stat changed resource resource
-	return p.gwClient.Stat(ownerCtx, &provider.StatRequest{Ref: ref})
+	return metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token), nil
 }
diff --git a/extensions/search/pkg/search/provider/searchprovider.go b/extensions/search/pkg/search/provider/searchprovider.go
index 9356af88a8e..94040563548 100644
--- a/extensions/search/pkg/search/provider/searchprovider.go
+++ b/extensions/search/pkg/search/provider/searchprovider.go
@@ -14,6 +14,7 @@ import (
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/errtypes"
+	sdk "github.com/cs3org/reva/v2/pkg/sdk/common"
 	"github.com/cs3org/reva/v2/pkg/storage/utils/walker"
 	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
@@ -57,6 +58,7 @@ func (p *Provider) Search(ctx context.Context, req *searchsvc.SearchRequest) (*s
 	if req.Query == "" {
 		return nil, errtypes.PreconditionFailed("empty query provided")
 	}
+	p.logger.Debug().Str("query", req.Query).Msg("performing a search")
 
 	listSpacesRes, err := p.gwClient.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
 		Filters: []*provider.ListStorageSpacesRequest_Filter{
@@ -67,27 +69,61 @@ func (p *Provider) Search(ctx context.Context, req *searchsvc.SearchRequest) (*s
 		},
 	})
 	if err != nil {
+		p.logger.Error().Err(err).Msg("failed to list the user's storage spaces")
 		return nil, err
 	}
 
+	mountpointMap := map[string]string{}
+	for _, space := range listSpacesRes.StorageSpaces {
+		if space.SpaceType != "mountpoint" {
+			continue
+		}
+		opaqueMap := sdk.DecodeOpaqueMap(space.Opaque)
+		grantSpaceId := storagespace.FormatResourceID(provider.ResourceId{
+			StorageId: opaqueMap["grantStorageID"],
+			OpaqueId:  opaqueMap["grantOpaqueID"],
+		})
+		mountpointMap[grantSpaceId] = space.Id.OpaqueId
+	}
+
 	matches := []*searchmsg.Match{}
 	for _, space := range listSpacesRes.StorageSpaces {
-		pathPrefix := ""
-		if space.SpaceType == "grant" {
+		var mountpointRootId *searchmsg.ResourceID
+		mountpointPrefix := ""
+		switch space.SpaceType {
+		case "mountpoint":
+			continue // mountpoint spaces are only "links" to the shared spaces. we have to search the shared "grant" space instead
+		case "grant":
+			mountpointId, ok := mountpointMap[space.Id.OpaqueId]
+			if !ok {
+				p.logger.Warn().Interface("space", space).Msg("could not find mountpoint space for grant space")
+				continue
+			}
 			gpRes, err := p.gwClient.GetPath(ctx, &provider.GetPathRequest{
 				ResourceId: space.Root,
 			})
 			if err != nil {
-				return nil, err
+				p.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Msg("failed to get path for grant space root")
+				continue
 			}
 			if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
-				return nil, errtypes.NewErrtypeFromStatus(gpRes.Status)
+				p.logger.Error().Interface("status", gpRes.Status).Str("space", space.Id.OpaqueId).Msg("failed to get path for grant space root")
+				continue
 			}
-			pathPrefix = utils.MakeRelativePath(gpRes.Path)
+			mountpointPrefix = utils.MakeRelativePath(gpRes.Path)
+			sid, oid, err := storagespace.SplitID(mountpointId)
+			if err != nil {
+				p.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Str("mountpointId", mountpointId).Msg("invalid mountpoint space id")
+				continue
+			}
+			mountpointRootId = &searchmsg.ResourceID{
+				StorageId: sid,
+				OpaqueId:  oid,
+			}
+			p.logger.Debug().Interface("grantSpace", space).Interface("mountpointRootId", mountpointRootId).Msg("searching a grant")
 		}
 
 		_, rootStorageID := storagespace.SplitStorageID(space.Root.StorageId)
-
 		res, err := p.indexClient.Search(ctx, &searchsvc.SearchIndexRequest{
 			Query: req.Query,
 			Ref: &searchmsg.Reference{
@@ -95,16 +131,21 @@ func (p *Provider) Search(ctx context.Context, req *searchsvc.SearchRequest) (*s
 					StorageId: space.Root.StorageId,
 					OpaqueId:  rootStorageID,
 				},
-				Path: pathPrefix,
+				Path: mountpointPrefix,
 			},
 		})
 		if err != nil {
+			p.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Msg("failed to search the index")
 			return nil, err
 		}
+		p.logger.Debug().Str("space", space.Id.OpaqueId).Int("hits", len(res.Matches)).Msg("space search done")
 
 		for _, match := range res.Matches {
-			if pathPrefix != "" {
-				match.Entity.Ref.Path = utils.MakeRelativePath(strings.TrimPrefix(match.Entity.Ref.Path, pathPrefix))
+			if mountpointPrefix != "" {
+				match.Entity.Ref.Path = utils.MakeRelativePath(strings.TrimPrefix(match.Entity.Ref.Path, mountpointPrefix))
+			}
+			if mountpointRootId != nil {
+				match.Entity.Ref.ResourceId = mountpointRootId
 			}
 			matches = append(matches, match)
 		}
diff --git a/extensions/search/pkg/search/search.go b/extensions/search/pkg/search/search.go
index 3e9867c0619..12df203422e 100644
--- a/extensions/search/pkg/search/search.go
+++ b/extensions/search/pkg/search/search.go
@@ -38,7 +38,7 @@ type ProviderClient interface {
 type IndexClient interface {
 	Search(ctx context.Context, req *searchsvc.SearchIndexRequest) (*searchsvc.SearchIndexResponse, error)
 	Add(ref *providerv1beta1.Reference, ri *providerv1beta1.ResourceInfo) error
-	Move(ri *providerv1beta1.ResourceInfo) error
+	Move(ri *providerv1beta1.ResourceInfo, path string) error
 	Delete(ri *providerv1beta1.ResourceId) error
 	Restore(ri *providerv1beta1.ResourceId) error
 	Purge(ri *providerv1beta1.ResourceId) error
diff --git a/extensions/search/pkg/service/v0/service.go b/extensions/search/pkg/service/v0/service.go
index b2e4873b1a2..ef589ec5ea4 100644
--- a/extensions/search/pkg/service/v0/service.go
+++ b/extensions/search/pkg/service/v0/service.go
@@ -46,7 +46,11 @@ func NewHandler(opts ...Option) (searchsvc.SearchProviderHandler, error) {
 	indexDir := filepath.Join(cfg.Datapath, "index.bleve")
 	bleveIndex, err := bleve.Open(indexDir)
 	if err != nil {
-		bleveIndex, err = bleve.New(indexDir, index.BuildMapping())
+		mapping, err := index.BuildMapping()
+		if err != nil {
+			return nil, err
+		}
+		bleveIndex, err = bleve.New(indexDir, mapping)
 		if err != nil {
 			return nil, err
 		}


Test patch:
diff --git a/extensions/search/pkg/search/index/index_test.go b/extensions/search/pkg/search/index/index_test.go
index 69f1a17e7f7..7c46c64b227 100644
--- a/extensions/search/pkg/search/index/index_test.go
+++ b/extensions/search/pkg/search/index/index_test.go
@@ -37,7 +37,7 @@ var _ = Describe("Index", func() {
 				StorageId: "storageid",
 				OpaqueId:  "someopaqueid",
 			},
-			Path:     "foo.pdf",
+			Path:     "Foo.pdf",
 			Size:     12345,
 			Type:     sprovider.ResourceType_RESOURCE_TYPE_FILE,
 			MimeType: "application/pdf",
@@ -45,7 +45,7 @@ var _ = Describe("Index", func() {
 		}
 		parentRef = &sprovider.Reference{
 			ResourceId: rootId,
-			Path:       "./sudbir",
+			Path:       "./my/sudbir",
 		}
 		parentRi = &sprovider.ResourceInfo{
 			Id: &sprovider.ResourceId{
@@ -59,7 +59,7 @@ var _ = Describe("Index", func() {
 		}
 		childRef = &sprovider.Reference{
 			ResourceId: rootId,
-			Path:       "./sudbir/child.pdf",
+			Path:       "./my/sudbir/child.pdf",
 		}
 		childRi = &sprovider.ResourceInfo{
 			Id: &sprovider.ResourceId{
@@ -92,8 +92,10 @@ var _ = Describe("Index", func() {
 	)
 
 	BeforeEach(func() {
-		var err error
-		bleveIndex, err = bleve.NewMemOnly(index.BuildMapping())
+		mapping, err := index.BuildMapping()
+		Expect(err).ToNot(HaveOccurred())
+
+		bleveIndex, err = bleve.NewMemOnly(mapping)
 		Expect(err).ToNot(HaveOccurred())
 
 		i, err = index.New(bleveIndex)
@@ -201,6 +203,21 @@ var _ = Describe("Index", func() {
 				}
 			})
 
+			It("is case-insensitive", func() {
+				res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{
+					Ref: &searchmsg.Reference{
+						ResourceId: &searchmsg.ResourceID{
+							StorageId: ref.ResourceId.StorageId,
+							OpaqueId:  ref.ResourceId.OpaqueId,
+						},
+					},
+					Query: "Foo*",
+				})
+				Expect(err).ToNot(HaveOccurred())
+				Expect(res).ToNot(BeNil())
+				Expect(len(res.Matches)).To(Equal(1))
+			})
+
 			Context("and an additional file in a subdirectory", func() {
 				var (
 					nestedRef *sprovider.Reference
@@ -349,7 +366,7 @@ var _ = Describe("Index", func() {
 			Expect(err).ToNot(HaveOccurred())
 
 			parentRi.Path = "newname"
-			err = i.Move(parentRi)
+			err = i.Move(parentRi, "./somewhere/else/newname")
 			Expect(err).ToNot(HaveOccurred())
 
 			assertDocCount(rootId, "subdir", 0)
@@ -365,7 +382,7 @@ var _ = Describe("Index", func() {
 			})
 			Expect(err).ToNot(HaveOccurred())
 			Expect(len(res.Matches)).To(Equal(1))
-			Expect(res.Matches[0].Entity.Ref.Path).To(Equal("./newname/child.pdf"))
+			Expect(res.Matches[0].Entity.Ref.Path).To(Equal("./somewhere/else/newname/child.pdf"))
 		})
 	})
 })
diff --git a/extensions/search/pkg/search/provider/events_test.go b/extensions/search/pkg/search/provider/events_test.go
new file mode 100644
index 00000000000..9de17c5df34
--- /dev/null
+++ b/extensions/search/pkg/search/provider/events_test.go
@@ -0,0 +1,175 @@
+package provider_test
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/mock"
+
+	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/events"
+	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
+	cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
+	"github.com/owncloud/ocis/v2/extensions/search/pkg/search/mocks"
+	provider "github.com/owncloud/ocis/v2/extensions/search/pkg/search/provider"
+	"github.com/owncloud/ocis/v2/ocis-pkg/log"
+)
+
+var _ = Describe("Searchprovider", func() {
+	var (
+		p           *provider.Provider
+		gwClient    *cs3mocks.GatewayAPIClient
+		indexClient *mocks.IndexClient
+
+		ctx        context.Context
+		eventsChan chan interface{}
+
+		logger = log.NewLogger()
+		user   = &userv1beta1.User{
+			Id: &userv1beta1.UserId{
+				OpaqueId: "user",
+			},
+		}
+
+		ref = &sprovider.Reference{
+			ResourceId: &sprovider.ResourceId{
+				StorageId: "storageid",
+				OpaqueId:  "rootopaqueid",
+			},
+			Path: "./foo.pdf",
+		}
+		ri = &sprovider.ResourceInfo{
+			Id: &sprovider.ResourceId{
+				StorageId: "storageid",
+				OpaqueId:  "opaqueid",
+			},
+			Path: "foo.pdf",
+			Size: 12345,
+		}
+	)
+
+	BeforeEach(func() {
+		ctx = context.Background()
+		eventsChan = make(chan interface{})
+		gwClient = &cs3mocks.GatewayAPIClient{}
+		indexClient = &mocks.IndexClient{}
+
+		p = provider.New(gwClient, indexClient, "", eventsChan, logger)
+
+		gwClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{
+			Status: status.NewOK(ctx),
+			Token:  "authtoken",
+		}, nil)
+		gwClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
+			Status: status.NewOK(context.Background()),
+			Info:   ri,
+		}, nil)
+		indexClient.On("DocCount").Return(uint64(1), nil)
+	})
+
+	Describe("New", func() {
+		It("returns a new instance", func() {
+			p = provider.New(gwClient, indexClient, "", eventsChan, logger)
+			Expect(p).ToNot(BeNil())
+		})
+	})
+
+	Describe("events", func() {
+		It("triggers an index update when a file has been uploaded", func() {
+			called := false
+			indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
+				return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
+			})).Return(nil).Run(func(args mock.Arguments) {
+				called = true
+			})
+			eventsChan <- events.FileUploaded{
+				Ref:       ref,
+				Executant: user.Id,
+			}
+
+			Eventually(func() bool {
+				return called
+			}, "2s").Should(BeTrue())
+		})
+
+		It("removes an entry from the index when the file has been deleted", func() {
+			called := false
+			gwClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
+				Status: status.NewNotFound(context.Background(), ""),
+			}, nil)
+			indexClient.On("Delete", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
+				return id.OpaqueId == ri.Id.OpaqueId
+			})).Return(nil).Run(func(args mock.Arguments) {
+				called = true
+			})
+			eventsChan <- events.ItemTrashed{
+				Ref:       ref,
+				ID:        ri.Id,
+				Executant: user.Id,
+			}
+
+			Eventually(func() bool {
+				return called
+			}, "2s").Should(BeTrue())
+		})
+
+		It("indexes items when they are being restored", func() {
+			called := false
+			indexClient.On("Restore", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
+				return id.OpaqueId == ri.Id.OpaqueId
+			})).Return(nil).Run(func(args mock.Arguments) {
+				called = true
+			})
+			eventsChan <- events.ItemRestored{
+				Ref:       ref,
+				Executant: user.Id,
+			}
+
+			Eventually(func() bool {
+				return called
+			}, "2s").Should(BeTrue())
+		})
+
+		It("indexes items when a version has been restored", func() {
+			called := false
+			indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
+				return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
+			})).Return(nil).Run(func(args mock.Arguments) {
+				called = true
+			})
+			eventsChan <- events.FileVersionRestored{
+				Ref:       ref,
+				Executant: user.Id,
+			}
+
+			Eventually(func() bool {
+				return called
+			}, "2s").Should(BeTrue())
+		})
+
+		It("indexes items when they are being moved", func() {
+			called := false
+			gwClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
+				Status: status.NewOK(ctx),
+				Path:   "./new/path.pdf",
+			}, nil)
+			indexClient.On("Move", mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
+				return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
+			}), "./new/path.pdf").Return(nil).Run(func(args mock.Arguments) {
+				called = true
+			})
+			ref.Path = "./new/path.pdf"
+			eventsChan <- events.ItemMoved{
+				Ref:       ref,
+				Executant: user.Id,
+			}
+
+			Eventually(func() bool {
+				return called
+			}, "2s").Should(BeTrue())
+		})
+	})
+})
diff --git a/extensions/search/pkg/search/provider/searchprovider_test.go b/extensions/search/pkg/search/provider/searchprovider_test.go
index 956d60df0a3..37f59ff1cdb 100644
--- a/extensions/search/pkg/search/provider/searchprovider_test.go
+++ b/extensions/search/pkg/search/provider/searchprovider_test.go
@@ -11,7 +11,6 @@ import (
 	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
-	"github.com/cs3org/reva/v2/pkg/events"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
 	cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
 	"github.com/owncloud/ocis/v2/extensions/search/pkg/search/mocks"
@@ -55,13 +54,6 @@ var _ = Describe("Searchprovider", func() {
 			Name: "personalspace",
 		}
 
-		ref = &sprovider.Reference{
-			ResourceId: &sprovider.ResourceId{
-				StorageId: "storageid",
-				OpaqueId:  "rootopaqueid",
-			},
-			Path: "./foo.pdf",
-		}
 		ri = &sprovider.ResourceInfo{
 			Id: &sprovider.ResourceId{
 				StorageId: "storageid",
@@ -98,98 +90,6 @@ var _ = Describe("Searchprovider", func() {
 		})
 	})
 
-	Describe("events", func() {
-		It("trigger an index update when a file has been uploaded", func() {
-			called := false
-			indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
-				return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
-			})).Return(nil).Run(func(args mock.Arguments) {
-				called = true
-			})
-			eventsChan <- events.FileUploaded{
-				Ref:       ref,
-				Executant: user.Id,
-			}
-
-			Eventually(func() bool {
-				return called
-			}, "2s").Should(BeTrue())
-		})
-
-		It("removes an entry from the index when the file has been deleted", func() {
-			called := false
-
-			gwClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
-				Status: status.NewNotFound(context.Background(), ""),
-			}, nil)
-			indexClient.On("Delete", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
-				return id.OpaqueId == ri.Id.OpaqueId
-			})).Return(nil).Run(func(args mock.Arguments) {
-				called = true
-			})
-			eventsChan <- events.ItemTrashed{
-				Ref:       ref,
-				ID:        ri.Id,
-				Executant: user.Id,
-			}
-
-			Eventually(func() bool {
-				return called
-			}, "2s").Should(BeTrue())
-		})
-
-		It("indexes items when they are being restored", func() {
-			called := false
-			indexClient.On("Restore", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
-				return id.OpaqueId == ri.Id.OpaqueId
-			})).Return(nil).Run(func(args mock.Arguments) {
-				called = true
-			})
-			eventsChan <- events.ItemRestored{
-				Ref:       ref,
-				Executant: user.Id,
-			}
-
-			Eventually(func() bool {
-				return called
-			}, "2s").Should(BeTrue())
-		})
-
-		It("indexes items when a version has been restored", func() {
-			called := false
-			indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
-				return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
-			})).Return(nil).Run(func(args mock.Arguments) {
-				called = true
-			})
-			eventsChan <- events.FileVersionRestored{
-				Ref:       ref,
-				Executant: user.Id,
-			}
-
-			Eventually(func() bool {
-				return called
-			}, "2s").Should(BeTrue())
-		})
-
-		It("indexes items when they are being moved", func() {
-			called := false
-			indexClient.On("Move", mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
-				return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
-			})).Return(nil).Run(func(args mock.Arguments) {
-				called = true
-			})
-			eventsChan <- events.ItemMoved{
-				Ref:       ref,
-				Executant: user.Id,
-			}
-
-			Eventually(func() bool {
-				return called
-			}, "2s").Should(BeTrue())
-		})
-	})
-
 	Describe("IndexSpace", func() {
 		It("walks the space and indexes all files", func() {
 			gwClient.On("GetUserByClaim", mock.Anything, mock.Anything).Return(&userv1beta1.GetUserByClaimResponse{
@@ -267,7 +167,8 @@ var _ = Describe("Searchprovider", func() {
 
 		Context("with received shares", func() {
 			var (
-				grantSpace *sprovider.StorageSpace
+				grantSpace      *sprovider.StorageSpace
+				mountpointSpace *sprovider.StorageSpace
 			)
 
 			BeforeEach(func() {
@@ -275,19 +176,32 @@ var _ = Describe("Searchprovider", func() {
 					SpaceType: "grant",
 					Owner:     otherUser,
 					Id:        &sprovider.StorageSpaceId{OpaqueId: "otherspaceroot!otherspacegrant"},
-					Root:      &sprovider.ResourceId{StorageId: "otherspaceroot", OpaqueId: "otherspaceroot"},
+					Root:      &sprovider.ResourceId{StorageId: "otherspaceroot", OpaqueId: "otherspacegrant"},
 					Name:      "grantspace",
 				}
+				mountpointSpace = &sprovider.StorageSpace{
+					SpaceType: "mountpoint",
+					Owner:     otherUser,
+					Id:        &sprovider.StorageSpaceId{OpaqueId: "otherspaceroot!otherspacemountpoint"},
+					Root:      &sprovider.ResourceId{StorageId: "otherspaceroot", OpaqueId: "otherspacemountpoint"},
+					Name:      "mountpointspace",
+					Opaque: &typesv1beta1.Opaque{
+						Map: map[string]*typesv1beta1.OpaqueEntry{
+							"grantStorageID": {Decoder: "plain", Value: []byte("otherspaceroot")},
+							"grantOpaqueID":  {Decoder: "plain", Value: []byte("otherspacegrant")},
+						},
+					},
+				}
 				gwClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
 					Status: status.NewOK(ctx),
 					Path:   "/grant/path",
 				}, nil)
 			})
 
-			It("searches the received spaces (grants)", func() {
+			It("searches the received spaces", func() {
 				gwClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
 					Status:        status.NewOK(ctx),
-					StorageSpaces: []*sprovider.StorageSpace{grantSpace},
+					StorageSpaces: []*sprovider.StorageSpace{grantSpace, mountpointSpace},
 				}, nil)
 				indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{
 					Matches: []*searchmsg.Match{
@@ -319,7 +233,7 @@ var _ = Describe("Searchprovider", func() {
 				match := res.Matches[0]
 				Expect(match.Entity.Id.OpaqueId).To(Equal("grant-shared-id"))
 				Expect(match.Entity.Name).To(Equal("Shared.pdf"))
-				Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(grantSpace.Root.OpaqueId))
+				Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(mountpointSpace.Root.OpaqueId))
 				Expect(match.Entity.Ref.Path).To(Equal("./to/Shared.pdf"))
 
 				indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
@@ -330,7 +244,7 @@ var _ = Describe("Searchprovider", func() {
 			It("finds matches in both the personal space AND the grant", func() {
 				gwClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
 					Status:        status.NewOK(ctx),
-					StorageSpaces: []*sprovider.StorageSpace{personalSpace, grantSpace},
+					StorageSpaces: []*sprovider.StorageSpace{personalSpace, grantSpace, mountpointSpace},
 				}, nil)
 				indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
 					return req.Ref.ResourceId.StorageId == grantSpace.Root.StorageId