feat(search): configurable query filter

This commit is contained in:
Florian Schade committed 2026-10-22 20:05:31 +02:00
1 parent 38262408d6
commit 3fe11ea60a
22 files changed
+851 -473

No files matched your search

+4 -1
View File
@@ -7,10 +7,13 @@
////////////////////////////////////////////////////////
AST <-
n:Nodes {
_ n:Nodes _ EOF {
return buildAST(n, c.text, c.pos)
}
EOF <-
!.
////////////////////////////////////////////////////////
// nodes
////////////////////////////////////////////////////////
+482 -421
View File
File diff suppressed because it is too large. Load diff
+38 -8
View File
@@ -33,13 +33,13 @@ func TestParse_Spec(t *testing.T) {
},
{
name: `AND`,
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
{
name: `AND cat AND dog`,
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
@@ -79,13 +79,13 @@ func TestParse_Spec(t *testing.T) {
},
{
name: `OR`,
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolOR},
},
},
{
name: `OR cat AND dog`,
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolOR},
},
},
@@ -941,28 +941,58 @@ func TestParse_Errors(t *testing.T) {
},
{
query: "animal:(AND cat)",
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
{
query: "animal:(OR cat)",
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolOR},
},
},
{
query: "(AND cat)",
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
{
query: "(OR cat)",
error: kql.StartsWithBinaryOperatorError{
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolOR},
},
},
{
query: `cat AND`,
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
{
query: `cat NOT`,
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolNOT},
},
},
{
query: `cat AND OR dog`,
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolOR},
},
},
{
query: `cat NOT AND dog`,
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
{
query: `cat AND (dog NOT)`,
error: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolNOT},
},
},
{
query: `cat dog`,
ast: &ast.Ast{
+35 -8
View File
@@ -7,13 +7,26 @@ import (
"github.com/opencloud-eu/opencloud/pkg/ast"
)
// StartsWithBinaryOperatorError records an error and the operation that caused it.
type StartsWithBinaryOperatorError struct {
// DanglingOperatorError records an error and the operator that caused it.
type DanglingOperatorError struct {
Node *ast.OperatorNode
}
func (e StartsWithBinaryOperatorError) Error() string {
return "the expression can't begin from a binary operator: '" + e.Node.Value + "'"
func (e DanglingOperatorError) Error() string {
return "the operator '" + e.Node.Value + "' is missing an operand"
}
// SyntaxError records an error and the query that caused it.
type SyntaxError struct {
Inner error
}
func (e SyntaxError) Error() string {
return "invalid query syntax: " + e.Inner.Error()
}
func (e SyntaxError) Unwrap() error {
return e.Inner
}
// NamedGroupInvalidNodesError records an error and the operation that caused it.
@@ -39,17 +52,31 @@ func (e UnsupportedTimeRangeError) Error() string {
return fmt.Sprintf("unable to convert '%v' to a time range", e.Value)
}
func isGrammarActionError(err error) bool {
var (
danglingOperator *DanglingOperatorError
namedGroupInvalidNodes *NamedGroupInvalidNodesError
unsupportedTimeRange *UnsupportedTimeRangeError
)
return errors.As(err, &danglingOperator) ||
errors.As(err, &namedGroupInvalidNodes) ||
errors.As(err, &unsupportedTimeRange)
}
// IsValidationError reports whether err is one of the KQL parse/validation
// errors produced by this package, i.e. the query itself is at fault and the
// caller should treat it as a bad request.
func IsValidationError(err error) bool {
var (
startsWithBinaryOperator *StartsWithBinaryOperatorError
namedGroupInvalidNodes *NamedGroupInvalidNodesError
unsupportedTimeRange *UnsupportedTimeRangeError
danglingOperator *DanglingOperatorError
syntax *SyntaxError
namedGroupInvalidNodes *NamedGroupInvalidNodesError
unsupportedTimeRange *UnsupportedTimeRangeError
)
return errors.As(err, &startsWithBinaryOperator) ||
return errors.As(err, &danglingOperator) ||
errors.As(err, &syntax) ||
errors.As(err, &namedGroupInvalidNodes) ||
errors.As(err, &unsupportedTimeRange)
}
+6 -9
View File
@@ -26,19 +26,16 @@ func (b Builder) Build(q string) (*ast.Ast, error) {
f, err := Parse("", []byte(q))
if err != nil {
var list errList
errors.As(err, &list)
for _, listError := range list {
var parserError *parserError
switch {
case errors.As(listError, &parserError):
if parserError.Inner != nil {
if errors.As(err, &list) {
for _, listError := range list {
var parserError *parserError
if errors.As(listError, &parserError) && isGrammarActionError(parserError.Inner) {
return nil, parserError.Inner
}
return nil, listError
}
}
return nil, &SyntaxError{Inner: err}
}
return f.(*ast.Ast), nil
+43 -1
View File
@@ -21,7 +21,7 @@ func TestNewAST(t *testing.T) {
{
name: "error",
givenQuery: kql.BoolAND,
expectedError: kql.StartsWithBinaryOperatorError{
expectedError: kql.DanglingOperatorError{
Node: &ast.OperatorNode{Value: kql.BoolAND},
},
},
@@ -50,3 +50,45 @@ func TestNewAST(t *testing.T) {
})
}
}
func TestBuild_ValidationErrors(t *testing.T) {
assert := tAssert.New(t)
for _, q := range []string{
"NOT",
"cat NOT",
"cat AND",
"AND cat",
"cat AND OR dog",
"cat NOT AND dog",
"(cat",
"cat)",
"()",
"cat) OR (dog",
} {
got, err := kql.Builder{}.Build(q)
assert.Nil(got, q)
assert.Error(err, q)
assert.True(kql.IsValidationError(err), "%q: %v", q, err)
}
}
func TestBuild_ValidQueries(t *testing.T) {
assert := tAssert.New(t)
for _, q := range []string{
"cat",
"cat dog",
"NOT cat",
"NOT NOT cat",
"cat NOT dog",
"cat AND NOT dog",
"(cat OR dog)",
`name:"cat dog"`,
"cat AND (dog OR fish)",
} {
got, err := kql.Builder{}.Build(q)
assert.NoError(err, q)
assert.NotNil(got, q)
}
}
+28 -14
View File
@@ -5,23 +5,12 @@ import (
)
func validateAst(a *ast.Ast) error {
switch node := a.Nodes[0].(type) {
case *ast.OperatorNode:
switch node.Value {
case BoolAND, BoolOR:
return &StartsWithBinaryOperatorError{Node: node}
}
}
return nil
return validateOperators(a.Nodes)
}
func validateGroupNode(n *ast.GroupNode) error {
switch node := n.Nodes[0].(type) {
case *ast.OperatorNode:
switch node.Value {
case BoolAND, BoolOR:
return &StartsWithBinaryOperatorError{Node: node}
}
if err := validateOperators(n.Nodes); err != nil {
return err
}
if n.Key != "" {
@@ -34,3 +23,28 @@ func validateGroupNode(n *ast.GroupNode) error {
return nil
}
func validateOperators(nodes []ast.Node) error {
for i, node := range nodes {
op, ok := node.(*ast.OperatorNode)
if !ok {
continue
}
switch op.Value {
case BoolAND, BoolOR:
if i == 0 {
return &DanglingOperatorError{Node: op}
}
if _, prevIsOperator := nodes[i-1].(*ast.OperatorNode); prevIsOperator {
return &DanglingOperatorError{Node: op}
}
}
if i == len(nodes)-1 {
return &DanglingOperatorError{Node: op}
}
}
return nil
}
+19
View File
@@ -80,6 +80,25 @@ Not all parts are supported, the following list gives an overview of parts that
In [this ADR](https://github.com/owncloud/ocis/blob/docs/ocis/adr/0020-file-search-query-language.md) you can read why KQL was chosen.
## Query filter
`SEARCH_QUERY_FILTER` is a KQL expression ANDed to every search. It applies on every engine, works on existing
indexes without reindexing, and keeps paging and totals right. Default:
```
NOT path:"./.space" AND NOT id:{{.SPACE_ROOT_PERSONAL}}
```
This leaves out `./.space` (the space image and readme) and the personal space root (named after its owner). Two
placeholders become the searched space's root id, or an id that matches nothing in other space types:
* `{{.SPACE_ROOT_PERSONAL}}` for personal spaces.
* `{{.SPACE_ROOT_PROJECT}}` for project spaces. Not in the default, so a project space stays findable by name; add
`AND NOT id:{{.SPACE_ROOT_PROJECT}}` to drop it too.
Quote the value in a shell or compose env, the braces are a template token. An empty value turns the filter off. The
service validates the filter at startup and does not start if it cannot parse it.
## Content analysis / Extraction
The search service supports the following content extraction methods:
+3 -1
View File
@@ -3,8 +3,9 @@ package config
import (
"context"
"github.com/opencloud-eu/opencloud/pkg/shared"
"go-micro.dev/v4/client"
"github.com/opencloud-eu/opencloud/pkg/shared"
)
// Config combines all available configuration parts.
@@ -29,6 +30,7 @@ type Config struct {
ContentExtractionSizeLimit uint64 `yaml:"content_extraction_size_limit" env:"SEARCH_CONTENT_EXTRACTION_SIZE_LIMIT" desc:"Maximum file size in bytes that is allowed for content extraction." introductionVersion:"1.0.0"`
BatchSize int `yaml:"batch_size" env:"SEARCH_BATCH_SIZE" desc:"The number of documents to process in a single batch. Defaults to 500." introductionVersion:"1.0.0"`
ReindexMaxConcurrency int `yaml:"reindex_concurrency" env:"SEARCH_REINDEX_MAX_CONCURRENCY" desc:"The maximum number of spaces that are reindexed concurrently when reindexing all spaces." introductionVersion:"7.4.0"`
QueryFilter string `yaml:"query_filter" env:"SEARCH_QUERY_FILTER" desc:"A KQL expression added to every search query with AND, e.g. to leave results out." introductionVersion:"%%NEXT%%"`
ServiceAccount ServiceAccount `yaml:"service_account"`
@@ -66,6 +66,7 @@ func DefaultConfig() *config.Config {
ContentExtractionSizeLimit: 20 * 1024 * 1024, // Limit content extraction to <20MB files by default
BatchSize: 50,
ReindexMaxConcurrency: 3,
QueryFilter: `NOT path:"./.space" AND NOT id:` + config.PersonalSpaceRootPlaceholder,
}
}
@@ -2,8 +2,11 @@ package parser
import (
"errors"
"fmt"
"strings"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/kql"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/search/pkg/config"
"github.com/opencloud-eu/opencloud/services/search/pkg/config/defaults"
@@ -49,5 +52,26 @@ func Validate(cfg *config.Config) error {
return shared.MissingServiceAccountSecret(cfg.Service.Name)
}
return validateQueryFilter(cfg.QueryFilter)
}
func validateQueryFilter(query string) error {
if query == "" {
return nil
}
expanded := strings.NewReplacer(
config.PersonalSpaceRootPlaceholder, `"`+config.NoResourceID+`"`,
config.ProjectSpaceRootPlaceholder, `"`+config.NoResourceID+`"`,
).Replace(query)
if strings.Contains(expanded, "{{") || strings.Contains(expanded, "}}") {
return errors.New("search query filter: unknown placeholder")
}
if _, err := (kql.Builder{}).Build(expanded); err != nil {
return fmt.Errorf("search query filter: %w", err)
}
return nil
}
@@ -0,0 +1,23 @@
package parser
import "testing"
func TestValidateQueryFilter(t *testing.T) {
for query, valid := range map[string]bool{
"": true,
`NOT path:"./.space" AND NOT id:{{.SPACE_ROOT_PERSONAL}}`: true,
`NOT id:{{.SPACE_ROOT_PERSONAL}} AND NOT id:{{.SPACE_ROOT_PROJECT}}`: true,
`NOT mediatype:folder`: true,
`NOT name:"a(b"`: true,
`NOT id:{{.SPACE_ROOT_GRANT}}`: false,
`NOT id:{{.SPACE_ROOT_PERSONAL}`: false,
`(NOT path:x`: false,
`NOT path:x) OR (name:y`: false,
`AND AND`: false,
`NOT`: false,
} {
if err := validateQueryFilter(query); (err == nil) != valid {
t.Errorf("validateQueryFilter(%q) = %v, want valid %v", query, err, valid)
}
}
}
+6
View File
@@ -2,6 +2,12 @@ package config
import "time"
const (
PersonalSpaceRootPlaceholder = "{{.SPACE_ROOT_PERSONAL}}"
ProjectSpaceRootPlaceholder = "{{.SPACE_ROOT_PROJECT}}"
NoResourceID = "none$none!none"
)
// Events combines the configuration options for the event bus.
type Events struct {
Disabled bool `yaml:"disabled" env:"SEARCH_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle GRPC requests." introductionVersion:"4.0.0"`
+22 -1
View File
@@ -321,6 +321,23 @@ Fixtures:
| VISIBILITY-06 | `hidden:banana` | no match | no match | no match | ✅ |
| VISIBILITY-07 | `hidden:"true"` | .private, dotfile.txt, secret.txt | .private, dotfile.txt, secret.txt | .private, dotfile.txt, secret.txt | ✅ |
### filter
Fixtures:
- `alice`, ID = 1$1!1, folder, Path = .
- `.space`, ID = 1$1!.space, folder
- `readme.md`, ID = 1$1!readme.md, Path = ./.space/readme.md
- `.spaces`, ID = 1$1!.spaces, folder
- `notes.md`, ID = 1$1!notes.md
| Case | Query | expected | bleve | OpenSearch | same? |
|---|---|---|---|---|---|
| FILTER-01 | `path:"./.space"` | .space, readme.md | .space, readme.md | .space, readme.md | ✅ |
| FILTER-02 | `(name:"*.md") AND (NOT path:"./....NOT id:"none$none!none")` | notes.md | notes.md | notes.md | ✅ |
| FILTER-03 | `(name:"alice" OR name:".spaces")...ace" AND NOT id:"1$1!1")` | .spaces | .spaces | .spaces | ✅ |
| FILTER-04 | `(name:"alice") AND (NOT path:"./...NOT id:"none$none!none")` | alice | alice | alice | ✅ |
### boolean
Fixtures:
@@ -448,7 +465,11 @@ Fixtures:
| Case | Query | expected | bleve | OpenSearch | same? |
|---|---|---|---|---|---|
| INVALID-01 | `AND mediatype:document` | bad request | bad request | bad request | ✅ |
| INVALID-02 | `mediatype:document AND` | alpha.txt | alpha.txt | alpha.txt | ✅ |
| INVALID-02 | `mediatype:document AND` | bad request | bad request | bad request | ✅ |
| INVALID-03 | `mediatype:document NOT` | bad request | bad request | bad request | ✅ |
| INVALID-04 | `mediatype:document AND OR name:alpha` | bad request | bad request | bad request | ✅ |
| INVALID-05 | `(mediatype:document` | bad request | bad request | bad request | ✅ |
| INVALID-06 | `mediatype:document) OR (name:alpha` | bad request | bad request | bad request | ✅ |
## Operations
@@ -104,6 +104,7 @@ func queryGroups() []queryGroup {
fieldsGroup(),
deletedGroup(),
visibilityGroup(),
filterGroup(),
booleanGroup(),
samenameGroup(),
stressGroup(),
@@ -0,0 +1,24 @@
package parity
import (
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
func filterGroup() queryGroup {
return queryGroup{
name: "filter",
fixtures: []search.Resource{
fixtureFolder("alice", withID("1$1!1"), withPath(".")),
fixtureFolder(".space", withPath("./.space")),
fixtureDoc("readme.md", withPath("./.space/readme.md")),
fixtureFolder(".spaces", withPath("./.spaces")),
fixtureDoc("notes.md"),
},
cases: []queryCase{
{id: 1, query: `path:"./.space"`, want: []string{".space", "readme.md"}},
{id: 2, query: `(name:"*.md") AND (NOT path:"./.space" AND NOT id:"none$none!none")`, want: []string{"notes.md"}},
{id: 3, query: `(name:"alice" OR name:".spaces") AND (NOT path:"./.space" AND NOT id:"1$1!1")`, want: []string{".spaces"}},
{id: 4, query: `(name:"alice") AND (NOT path:"./.space" AND NOT id:"none$none!none")`, want: []string{"alice"}},
},
}
}
@@ -12,7 +12,11 @@ func invalidGroup() queryGroup {
},
cases: []queryCase{
{id: 1, query: `AND mediatype:document`, wantBadRequest: true},
{id: 2, query: `mediatype:document AND`, want: []string{"alpha.txt"}},
{id: 2, query: `mediatype:document AND`, wantBadRequest: true},
{id: 3, query: `mediatype:document NOT`, wantBadRequest: true},
{id: 4, query: `mediatype:document AND OR name:alpha`, wantBadRequest: true},
{id: 5, query: `(mediatype:document`, wantBadRequest: true},
{id: 6, query: `mediatype:document) OR (name:alpha`, wantBadRequest: true},
},
}
}
+36 -1
View File
@@ -30,6 +30,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/opencloud-eu/opencloud/pkg/kql"
"github.com/opencloud-eu/opencloud/pkg/log"
searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
@@ -75,6 +76,8 @@ type Service struct {
serviceAccountSecret string
batchSize int
queryFilter string
}
var errSkipSpace error
@@ -92,11 +95,37 @@ func NewService(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], eng E
serviceAccountSecret: cfg.ServiceAccount.ServiceAccountSecret,
batchSize: cfg.BatchSize,
queryFilter: cfg.QueryFilter,
}
return s
}
// withQueryFilter adds the query filter to the engine query, so paging and totals stay right
func (s *Service) withQueryFilter(query string, space *provider.StorageSpace) string {
if s.queryFilter == "" {
return query
}
return "(" + query + ") AND (" + ExpandQueryFilter(s.queryFilter, space) + ")"
}
// ExpandQueryFilter fills the query filter placeholders for the given space.
func ExpandQueryFilter(query string, space *provider.StorageSpace) string {
rootID := func(spaceType string) string {
if space.GetSpaceType() == spaceType {
return `"` + storagespace.FormatResourceID(space.GetRoot()) + `"`
}
return `"` + config.NoResourceID + `"`
}
return strings.NewReplacer(
config.PersonalSpaceRootPlaceholder, rootID(_spaceTypePersonal),
config.ProjectSpaceRootPlaceholder, rootID(_spaceTypeProject),
).Replace(query)
}
// Search processes a search request and passes it down to the engine.
func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) {
s.logger.Debug().Str("query", req.Query).Msg("performing a search")
@@ -136,6 +165,12 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se
if query == "" {
return nil, errtypes.BadRequest("empty query provided")
}
if _, err := (kql.Builder{}).Build(query); err != nil {
if kql.IsValidationError(err) {
return nil, errtypes.BadRequest(err.Error())
}
return nil, err
}
req.Query = query
if len(scope) > 0 {
scopedID, err := storagespace.ParseID(scope)
@@ -413,7 +448,7 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
}
searchRequest := &searchsvc.SearchIndexRequest{
Query: req.Query,
Query: s.withQueryFilter(req.Query, space),
Ref: &searchmsg.Reference{
ResourceId: searchRootID,
Path: searchPathPrefix,
@@ -177,6 +177,19 @@ var _ = Describe("Searchprovider", func() {
)
})
DescribeTable("ExpandQueryFilter",
func(spaceType, expected string) {
space := &sprovider.StorageSpace{
SpaceType: spaceType,
Root: &sprovider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "spaceid"},
}
Expect(search.ExpandQueryFilter(`NOT id:{{.SPACE_ROOT_PERSONAL}} AND NOT id:{{.SPACE_ROOT_PROJECT}}`, space)).To(Equal(expected))
},
Entry("a personal space fills the personal placeholder", "personal", `NOT id:"storageid$spaceid!spaceid" AND NOT id:"none$none!none"`),
Entry("a project space fills the project placeholder", "project", `NOT id:"none$none!none" AND NOT id:"storageid$spaceid!spaceid"`),
Entry("a share fills neither", "grant", `NOT id:"none$none!none" AND NOT id:"none$none!none"`),
)
DescribeTable("IsHidden",
func(path string, hidden bool) {
Expect(search.IsHidden(path)).To(Equal(hidden))
@@ -244,6 +257,17 @@ var _ = Describe("Searchprovider", func() {
}))
})
It("adds the query filter with the personal space root filled in", func() {
s := search.NewService(gatewaySelector, indexClient, extractor, nil, logger, &config.Config{
QueryFilter: `NOT path:"./.space" AND NOT id:{{.SPACE_ROOT_PERSONAL}}`,
})
_, err := s.Search(ctx, &searchsvc.SearchRequest{Query: "foo"})
Expect(err).ToNot(HaveOccurred())
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
return req.Query == `(foo) AND (NOT path:"./.space" AND NOT id:"storageid$personalspace!personalspace")`
}))
})
It("searches the personal user space", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo",
@@ -100,12 +100,12 @@ Feature: Search
@issue-10329
Scenario Outline: user can search hidden files
Given using <dav-path-version> DAV path
And user "Alice" has created a folder ".space" in space "project101"
When user "Alice" searches for "*.sp*" using the WebDAV API
And user "Alice" has created a folder ".hidden" in space "project101"
When user "Alice" searches for "*.hid*" using the WebDAV API
Then the HTTP status code should be "207"
And the search result should contain "1" entries
And the search result of user "Alice" should contain these entries:
| /.space |
| /.hidden |
Examples:
| dav-path-version |
| old |
@@ -113,6 +113,28 @@ Feature: Search
| spaces |
Scenario Outline: user cannot search the .space folder of a space
Given using <dav-path-version> DAV path
And user "Alice" has created a folder ".space" in space "project101"
When user "Alice" searches for "*.sp*" using the WebDAV API
Then the HTTP status code should be "207"
And the search result should contain "0" entries
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario: user cannot search files inside the .space folder of a space
Given using spaces DAV path
And user "Alice" has created a folder ".space" in space "project101"
And user "Alice" has uploaded a file inside space "project101" with content "hidden content" to ".space/spaceInternal.txt"
When user "Alice" searches for "*spaceInternal*" using the WebDAV API
Then the HTTP status code should be "207"
And the search result should contain "0" entries
Scenario Outline: user cannot search pending share
Given user "Brian" has disabled auto-accepting
And using <dav-path-version> DAV path
@@ -184,9 +184,8 @@ Feature: media type search
And user "Alice" has uploaded file "filesForUpload/data.tar.bz2" to "data.tar.bz2"
When user "Alice" searches for "mediatype:folder" using the WebDAV API
Then the HTTP status code should be "207"
And the search result should contain "2" entries
And the search result should contain "1" entries
And the search result of user "Alice" should contain these entries:
| %spaceid% |
| testFolder |
When user "Alice" searches for "mediatype:document" using the WebDAV API
Then the HTTP status code should be "207"
@@ -261,5 +261,4 @@ Feature: tag search
| (tag:mathe OR tag:klass10) NOT tag:physik | 2 | answers | verification work |
| tag:mathe NOT name:exercises | 1 | answers | |
| tag:mathe AND NOT name:exercises | 1 | answers | |
# The third finding is the personal space itself
| NOT tag:mathe | 3 | verification work | withoutTagFolder |
| NOT tag:mathe | 2 | verification work | withoutTagFolder |