Repository: owncloud/ocis
Pull request #5017 (2022-11-09 06:03:36)

Bump github.com/go-micro/plugins/v4/registry/consul from 1.1.0 to 1.2.0
Bumps [github.com/go-micro/plugins/v4/registry/consul](https://github.com/go-micro/plugins) from 1.1.0 to 1.2.0.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/go-micro/plugins/compare/v3/auth/jwt/v1.1.0...v4/auth/jwt/v1.2.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-micro/plugins/v4/registry/consul&package-manager=go_modules&previous-version=1.1.0&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5017)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5017&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5017&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5017&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5017&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5017&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5017&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/.drone.env b/.drone.env
index a350fb74ab0..751093365f1 100644
--- a/.drone.env
+++ b/.drone.env
@@ -1,5 +1,5 @@
 # The test runner source for API tests
-CORE_COMMITID=7296d4f3544a0de278d8d2eee7388b6c44160724
+CORE_COMMITID=ff3c509f6956ed6d1b51dab63176b122c2027cb0
 CORE_BRANCH=master
 
 # The test runner source for UI tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c1e0172ebd..fbc54469ee8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ The following sections list the changes for unreleased.
 ## Summary
 
 * Bugfix - Do not reindex a space twice at the same time: [#5001](https://github.com/owncloud/ocis/pull/5001)
+* Bugfix - Find spaces by their name: [#5044](https://github.com/owncloud/ocis/pull/5044)
 * Bugfix - Fix notifications Web UI url: [#4998](https://github.com/owncloud/ocis/pull/4998)
 * Enhancement - Add the "hidden" state to the search index: [#5018](https://github.com/owncloud/ocis/pull/5018)
 * Enhancement - Remove windows from ci & release makefile: [#5026](https://github.com/owncloud/ocis/pull/5026)
@@ -20,6 +21,13 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/pull/5001
 
+* Bugfix - Find spaces by their name: [#5044](https://github.com/owncloud/ocis/pull/5044)
+
+   We've fixed finding spaces by their name in the search service.
+
+   https://github.com/owncloud/ocis/issues/4506
+   https://github.com/owncloud/ocis/pull/5044
+
 * Bugfix - Fix notifications Web UI url: [#4998](https://github.com/owncloud/ocis/pull/4998)
 
    We've fixed the configuration of the notification service's Web UI url that appears in emails.
diff --git a/changelog/unreleased/find-spaces.md b/changelog/unreleased/find-spaces.md
new file mode 100644
index 00000000000..fe50b7087db
--- /dev/null
+++ b/changelog/unreleased/find-spaces.md
@@ -0,0 +1,6 @@
+Bugfix: Find spaces by their name
+
+We've fixed finding spaces by their name in the search service.
+
+https://github.com/owncloud/ocis/pull/5044
+https://github.com/owncloud/ocis/issues/4506
diff --git a/go.mod b/go.mod
index c7bac66e687..6c7985fa624 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 	github.com/go-micro/plugins/v4/client/grpc v1.1.0
 	github.com/go-micro/plugins/v4/events/natsjs v1.1.0
 	github.com/go-micro/plugins/v4/logger/zerolog v1.1.0
-	github.com/go-micro/plugins/v4/registry/consul v1.1.0
+	github.com/go-micro/plugins/v4/registry/consul v1.2.0
 	github.com/go-micro/plugins/v4/registry/etcd v1.2.0
 	github.com/go-micro/plugins/v4/registry/kubernetes v1.1.0
 	github.com/go-micro/plugins/v4/registry/mdns v1.1.0
diff --git a/go.sum b/go.sum
index b834068b487..4e99fb479f4 100644
--- a/go.sum
+++ b/go.sum
@@ -478,8 +478,8 @@ github.com/go-micro/plugins/v4/events/natsjs v1.1.0 h1:eQ3JbN0mnLuiExBM29pRi/Ehl
 github.com/go-micro/plugins/v4/events/natsjs v1.1.0/go.mod h1:ibNwJyP1Z5y8unW4sryIn8EkdwGkrcaURGwm0DVeMcY=
 github.com/go-micro/plugins/v4/logger/zerolog v1.1.0 h1:sG20ktK9YN/h0TlmQXTuL4/yavxBy5QbzifcVARN5ww=
 github.com/go-micro/plugins/v4/logger/zerolog v1.1.0/go.mod h1:lUEQAHt8I+gJDmx3DbpU7aqwM3+MwFmFRnATxlp650w=
-github.com/go-micro/plugins/v4/registry/consul v1.1.0 h1:YZQ4ddLOK7YsfyjIHiA0SEDzaDYc6vVXrV+8Z6oF5aY=
-github.com/go-micro/plugins/v4/registry/consul v1.1.0/go.mod h1:57TUMkEggnfVEFoWA5JH/nCYCW/c4oRZHC+Ureecgs8=
+github.com/go-micro/plugins/v4/registry/consul v1.2.0 h1:nqrTzfWUTWKAy+M+i2FazbHnQn4m77EOtoch57kbCH4=
+github.com/go-micro/plugins/v4/registry/consul v1.2.0/go.mod h1:wTat7/K9XQ+i64VbbcMYFcEwipYfSgJM51HcA/sgsM4=
 github.com/go-micro/plugins/v4/registry/etcd v1.2.0 h1:tcHlU1GzvX3oZa8WQH8ylMCGie5qD5g98YWTESJjeqQ=
 github.com/go-micro/plugins/v4/registry/etcd v1.2.0/go.mod h1:CQeTHkjN3xMtIQsynaTTquMz2sHEdsTfRIfFzrX7aug=
 github.com/go-micro/plugins/v4/registry/kubernetes v1.1.0 h1:JfkrimhACDviPtKT7Fhqms7oeM9sgy0I37bzvn4AsI4=
diff --git a/services/search/pkg/search/index/index.go b/services/search/pkg/search/index/index.go
index fd1fb374da2..3e6842766fa 100644
--- a/services/search/pkg/search/index/index.go
+++ b/services/search/pkg/search/index/index.go
@@ -293,7 +293,7 @@ func toEntity(ref *sprovider.Reference, ri *sprovider.ResourceInfo) *indexDocume
 		Path:     ref.Path,
 		ID:       idToBleveId(ri.Id),
 		ParentID: idToBleveId(ri.ParentId),
-		Name:     ri.Path,
+		Name:     ri.Name,
 		Size:     ri.Size,
 		MimeType: ri.MimeType,
 		Type:     uint64(ri.Type),
diff --git a/services/settings/pkg/service/v0/service.go b/services/settings/pkg/service/v0/service.go
index 93673f52701..33c3cadd085 100644
--- a/services/settings/pkg/service/v0/service.go
+++ b/services/settings/pkg/service/v0/service.go
@@ -563,9 +563,14 @@ func (g Service) hasStaticPermission(ctx context.Context, permissionID string) b
 			return false
 		}
 
-		roleIDs = make([]string, 0, len(assignments))
+		// deduplicate roleids
+		uniqueRoleIds := make(map[string]struct{})
 		for _, a := range assignments {
-			roleIDs = append(roleIDs, a.GetRoleId())
+			uniqueRoleIds[a.GetRoleId()] = struct{}{}
+		}
+		roleIDs = make([]string, 0, len(uniqueRoleIds))
+		for a := range uniqueRoleIds {
+			roleIDs = append(roleIDs, a)
 		}
 	}
 	p, err := g.manager.ReadPermissionByID(permissionID, roleIDs)
diff --git a/services/settings/pkg/service/v0/settings.go b/services/settings/pkg/service/v0/settings.go
index 13784ba68bf..481ff078449 100644
--- a/services/settings/pkg/service/v0/settings.go
+++ b/services/settings/pkg/service/v0/settings.go
@@ -24,7 +24,7 @@ const (
 	RoleManagementPermissionName string = "role-management"
 
 	// SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission
-	SettingsManagementPermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58"
+	SettingsManagementPermissionID string = "3d58f441-4a05-42f8-9411-ef5874528ae1"
 	// SettingsManagementPermissionName is the hardcoded setting name for the settings management permission
 	SettingsManagementPermissionName string = "settings-management"
 
diff --git a/services/settings/pkg/store/defaults/defaults.go b/services/settings/pkg/store/defaults/defaults.go
index a68b35087ec..906ce680024 100644
--- a/services/settings/pkg/store/defaults/defaults.go
+++ b/services/settings/pkg/store/defaults/defaults.go
@@ -24,7 +24,7 @@ const (
 	RoleManagementPermissionName string = "role-management"
 
 	// SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission
-	SettingsManagementPermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58"
+	SettingsManagementPermissionID string = "3d58f441-4a05-42f8-9411-ef5874528ae1"
 	// SettingsManagementPermissionName is the hardcoded setting name for the settings management permission
 	SettingsManagementPermissionName string = "settings-management"
 
diff --git a/services/settings/pkg/store/metadata/bundles.go b/services/settings/pkg/store/metadata/bundles.go
index 36cdd7c96df..5e25ac53877 100644
--- a/services/settings/pkg/store/metadata/bundles.go
+++ b/services/settings/pkg/store/metadata/bundles.go
@@ -9,15 +9,10 @@ import (
 
 	"github.com/gofrs/uuid"
 	settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
-	"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
 )
 
 // ListBundles returns all bundles in the dataPath folder that match the given type.
 func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []string) ([]*settingsmsg.Bundle, error) {
-	// TODO: this is needed for initialization - we need to find a better way to fix this
-	if s.mdc == nil && len(bundleIDs) == 1 {
-		return defaultBundle(bundleType, bundleIDs[0]), nil
-	}
 	s.Init()
 	ctx := context.TODO()
 
@@ -52,9 +47,6 @@ func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []stri
 
 // ReadBundle tries to find a bundle by the given id from the metadata service
 func (s *Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) {
-	if s.mdc == nil {
-		return defaultBundle(settingsmsg.Bundle_TYPE_ROLE, bundleID)[0], nil
-	}
 	s.Init()
 	ctx := context.TODO()
 	b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID))
@@ -134,13 +126,3 @@ func (s *Store) RemoveSettingFromBundle(bundleID string, settingID string) error
 func bundlePath(id string) string {
 	return fmt.Sprintf("%s/%s", bundleFolderLocation, id)
 }
-
-func defaultBundle(bundleType settingsmsg.Bundle_Type, bundleID string) []*settingsmsg.Bundle {
-	var bundles []*settingsmsg.Bundle
-	for _, b := range defaults.GenerateBundlesDefaultRoles() {
-		if b.Type == bundleType && b.Id == bundleID {
-			bundles = append(bundles, b)
-		}
-	}
-	return bundles
-}


Test patch:
diff --git a/services/search/pkg/search/index/index_test.go b/services/search/pkg/search/index/index_test.go
index 66e363520ac..748b1aeddcd 100644
--- a/services/search/pkg/search/index/index_test.go
+++ b/services/search/pkg/search/index/index_test.go
@@ -44,6 +44,7 @@ var _ = Describe("Index", func() {
 				OpaqueId:  "myopaqueid",
 			},
 			Path:  "sub d!r",
+			Name:  "sub d!r",
 			Size:  12345,
 			Type:  sprovider.ResourceType_RESOURCE_TYPE_CONTAINER,
 			Mtime: &typesv1beta1.Timestamp{Seconds: 4000},
@@ -64,6 +65,7 @@ var _ = Describe("Index", func() {
 				OpaqueId:  "parentopaqueid",
 			},
 			Path:  "child.pdf",
+			Name:  "child.pdf",
 			Size:  12345,
 			Type:  sprovider.ResourceType_RESOURCE_TYPE_FILE,
 			Mtime: &typesv1beta1.Timestamp{Seconds: 4000},
@@ -116,6 +118,7 @@ var _ = Describe("Index", func() {
 				OpaqueId:  "someopaqueid",
 			},
 			Path:     filename,
+			Name:     filename,
 			Size:     12345,
 			Type:     sprovider.ResourceType_RESOURCE_TYPE_FILE,
 			MimeType: "application/pdf",
@@ -159,7 +162,7 @@ var _ = Describe("Index", func() {
 
 		Context("by filename", func() {
 			It("finds files with spaces in the filename", func() {
-				ri.Path = "Foo oo.pdf"
+				ri.Name = "Foo oo.pdf"
 				ref.Path = "./" + ri.Path
 				err := i.Add(ref, ri)
 				Expect(err).ToNot(HaveOccurred())
@@ -168,7 +171,7 @@ var _ = Describe("Index", func() {
 			})
 
 			It("finds files by digits in the filename", func() {
-				ri.Path = "12345.pdf"
+				ri.Name = "12345.pdf"
 				ref.Path = "./" + ri.Path
 				err := i.Add(ref, ri)
 				Expect(err).ToNot(HaveOccurred())
@@ -178,6 +181,7 @@ var _ = Describe("Index", func() {
 
 			It("filters hidden files", func() {
 				ri.Path = ".hidden.pdf"
+				ri.Name = ".hidden.pdf"
 				ref.Path = "./" + ri.Path
 				err := i.Add(ref, ri)
 				Expect(err).ToNot(HaveOccurred())
@@ -273,6 +277,7 @@ var _ = Describe("Index", func() {
 								OpaqueId:  "nestedopaqueid",
 							},
 							Path: "nestedpdf.pdf",
+							Name: "nestedpdf.pdf",
 							Size: 12345,
 						}
 						err := i.Add(nestedRef, nestedRI)
diff --git a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
index 52e5357111e..9980be49013 100644
--- a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
@@ -32,9 +32,6 @@ The expected failures in this file are from features in the owncloud/ocis repo.
 - [apiSpacesShares/copySpaces.feature:487](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/copySpaces.feature#L487)
 - [apiSpacesShares/copySpaces.feature:501](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/copySpaces.feature#L501)
 
-### [search doesn't find the project space by name](https://github.com/owncloud/ocis/issues/4506)
-- [apiSpaces/search.feature:96](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpaces/search.feature#L96)
-
 #### [PATCH request for TUS upload with wrong checksum gives incorrect response](https://github.com/owncloud/ocis/issues/1755)
 - [apiSpacesShares/shareUploadTUS.feature:204](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L204)
 - [apiSpacesShares/shareUploadTUS.feature:219](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L219)
@@ -44,3 +41,6 @@ The expected failures in this file are from features in the owncloud/ocis repo.
 - [apiSpacesShares/moveSpaces.feature:306](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L306)
 - [apiSpacesShares/copySpaces.feature:710](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/copySpaces.feature#L710)
 - [apiSpacesShares/copySpaces.feature:748](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiSpacesShares/copySpaces.feature#L748)
+
+### [Creating group with empty name returns status code 200](https://github.com/owncloud/ocis/issues/5050)
+- [apiGraph/createGroup.feature:40](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/createGroup.feature#L40)
diff --git a/tests/acceptance/features/apiGraph/createGroup.feature b/tests/acceptance/features/apiGraph/createGroup.feature
index afc1306a452..78f0e6968fc 100644
--- a/tests/acceptance/features/apiGraph/createGroup.feature
+++ b/tests/acceptance/features/apiGraph/createGroup.feature
@@ -34,4 +34,9 @@ Feature: create group
     Given user "Brian" has been created with default attributes and without skeleton files
     When user "Brian" tries to create a group "mygroup" using the Graph API
     And the HTTP status code should be "401"
-    And group "mygroup" should not exist
\ No newline at end of file
+    And group "mygroup" should not exist
+
+
+  Scenario: admin user tries to create a group that is the empty string
+    When user "Alice" tries to create a group "" using the Graph API
+    Then the HTTP status code should be "400"
\ No newline at end of file
diff --git a/tests/acceptance/features/apiGraph/getGroup.feature b/tests/acceptance/features/apiGraph/getGroup.feature
new file mode 100644
index 00000000000..d4fc69ac727
--- /dev/null
+++ b/tests/acceptance/features/apiGraph/getGroup.feature
@@ -0,0 +1,54 @@
+@api @skipOnOcV10
+Feature: get groups and their members
+  As an admin
+  I want to be able to get groups
+  So that I can see all the groups and their members
+
+  Background:
+    Given user "Alice" has been created with default attributes and without skeleton files
+    And the administrator has given "Alice" the role "Admin" using the settings api
+
+
+  Scenario: admin user lists all the groups
+    Given group "tea-lover" has been created
+    And group "coffee-lover" has been created
+    And group "h2o-lover" has been created
+    When user "Alice" gets all the groups using the Graph API
+    Then the HTTP status code should be "200"
+    And the extra groups returned by the API should be
+      | tea-lover    |
+      | coffee-lover |
+      | h2o-lover    |
+
+
+  Scenario: normal user cannot get the groups list
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And group "tea-lover" has been created
+    And group "coffee-lover" has been created
+    And group "h2o-lover" has been created
+    When user "Brian" gets all the groups using the Graph API
+    Then the HTTP status code should be "401"
+    And the last response should be an unauthorized response
+
+
+  Scenario: admin user gets users of a group
+    Given these users have been created with default attributes and without skeleton files:
+      | username |
+      | Brian    |
+      | Carol    |
+    And group "tea-lover" has been created
+    And user "Brian" has been added to group "tea-lover"
+    And user "Carol" has been added to group "tea-lover"
+    When user "Alice" gets all the members of group "tea-lover" using the Graph API
+    Then the HTTP status code should be "200"
+    And the users returned by the API should be
+      | Brian |
+      | Carol |
+
+
+  Scenario: normal user tries to get users of a group
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And group "tea-lover" has been created
+    When user "Brian" gets all the members of group "tea-lover" using the Graph API
+    Then the HTTP status code should be "401"
+    And the last response should be an unauthorized response
diff --git a/tests/acceptance/features/bootstrap/GraphContext.php b/tests/acceptance/features/bootstrap/GraphContext.php
index 59a48c56667..35d6a0e199b 100644
--- a/tests/acceptance/features/bootstrap/GraphContext.php
+++ b/tests/acceptance/features/bootstrap/GraphContext.php
@@ -318,19 +318,85 @@ class GraphContext implements Context {
 	}
 
 	/**
-	 * returns list of all groups
 	 *
-	 * @return array
+	 * @param array $groups
+	 *
+	 * @return void
+	 * @throws Exception
+	 */
+	public function theseGroupsShouldBeInTheResponse(array $groups): void {
+		$respondedGroups = $this->getArrayOfGroupsResponded($this->featureContext->getResponse());
+		foreach ($groups as $group) {
+			$found = false;
+			foreach ($respondedGroups as $respondedGroup) {
+				if ($respondedGroup["displayName"] === $group) {
+					$found = true;
+					break;
+				}
+			}
+			Assert::assertTrue($found, "Group '$group' not found in the list");
+		}
+	}
+
+	/**
+	 *
+	 * @param array $users
+	 *
+	 * @return void
 	 * @throws Exception
+	 */
+	public function theseUsersShouldBeInTheResponse(array $users): void {
+		$respondedUsers = $this->getArrayOfUsersResponded($this->featureContext->getResponse());
+		foreach ($users as $user) {
+			$found = false;
+			foreach ($respondedUsers as $respondedUser) {
+				if ($respondedUser["onPremisesSamAccountName"] === $user) {
+					$found = true;
+					break;
+				}
+			}
+			Assert::assertTrue($found, "User '$user' not found in the list");
+		}
+	}
+
+	/**
+	 *
+	 * @param string|null $user
+	 *
+	 * @return array
+	 */
+	public function getAdminOrUserCredentials(?string $user): array {
+		$credentials["username"] = $user ? $this->featureContext->getActualUsername($user) : $this->featureContext->getAdminUsername();
+		$credentials["password"] = $user ? $this->featureContext->getPasswordForUser($user) : $this->featureContext->getAdminPassword();
+		return $credentials;
+	}
+	/**
+	 *
+	 * @param string|null $user
+	 *
+	 * @return ResponseInterface
 	 * @throws GuzzleException
 	 */
-	public function adminHasRetrievedGroupListUsingTheGraphApi(): array {
-		$response =  GraphHelper::getGroups(
+	public function listGroups(?string $user = null): ResponseInterface {
+		$credentials = $this->getAdminOrUserCredentials($user);
+
+		return GraphHelper::getGroups(
 			$this->featureContext->getBaseUrl(),
 			$this->featureContext->getStepLineRef(),
-			$this->featureContext->getAdminUsername(),
-			$this->featureContext->getAdminPassword()
+			$credentials["username"],
+			$credentials["password"]
 		);
+	}
+
+	/**
+	 * returns list of groups
+	 *
+	 * @param ResponseInterface $response
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function getArrayOfGroupsResponded(ResponseInterface $response): array {
 		if ($response->getStatusCode() === 200) {
 			$jsonResponseBody = $this->featureContext->getJsonDecodedResponse($response);
 			return $jsonResponseBody["value"];
@@ -340,29 +406,64 @@ class GraphContext implements Context {
 	}
 
 	/**
-	 * returns a list of members in group
-	 *
-	 * @param string $group
 	 *
 	 * @return array
 	 * @throws Exception
 	 * @throws GuzzleException
 	 */
-	public function theAdminHasRetrievedMembersListOfGroupUsingTheGraphApi(string $group): array {
-		$response = GraphHelper::getMembersList(
+	public function adminHasRetrievedGroupListUsingTheGraphApi(): array {
+		return  $this->getArrayOfGroupsResponded($this->listGroups());
+	}
+
+	/**
+	 *
+	 * @param string $group
+	 * @param string|null $user
+	 *
+	 * @return ResponseInterface
+	 * @throws GuzzleException
+	 */
+	public function listGroupMembers(string $group, ?string $user = null): ResponseInterface {
+		$credentials = $this->getAdminOrUserCredentials($user);
+
+		return GraphHelper::getMembersList(
 			$this->featureContext->getBaseUrl(),
 			$this->featureContext->getStepLineRef(),
-			$this->featureContext->getAdminUsername(),
-			$this->featureContext->getAdminPassword(),
+			$credentials["username"],
+			$credentials["password"],
 			$this->featureContext->getAttributeOfCreatedGroup($group, 'id')
 		);
+	}
+
+	/**
+	 * returns list of users of a group
+	 *
+	 * @param ResponseInterface $response
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function getArrayOfUsersResponded(ResponseInterface $response): array {
 		if ($response->getStatusCode() === 200) {
 			return $this->featureContext->getJsonDecodedResponse($response);
 		} else {
-			$this->throwHttpException($response, "Could not retrieve members list for group $group.");
+			$this->throwHttpException($response, "Could not retrieve group members list.");
 		}
 	}
 
+	/**
+	 * returns a list of members in group
+	 *
+	 * @param string $group
+	 *
+	 * @return array
+	 * @throws Exception
+	 * @throws GuzzleException
+	 */
+	public function theAdminHasRetrievedMembersListOfGroupUsingTheGraphApi(string $group): array {
+		return $this->getArrayOfUsersResponded($this->listGroupMembers($group));
+	}
+
 	/**
 	 * creates a user with provided data
 	 * actor: the administrator
@@ -476,18 +577,13 @@ class GraphContext implements Context {
 	 * @throws GuzzleException
 	 */
 	public function createGroup(string $group, ?string $user = null): ResponseInterface {
-		if ($user) {
-			$username = $user;
-			$password = $this->featureContext->getPasswordForUser($user);
-		} else {
-			$username = $this->featureContext->getAdminUsername();
-			$password = $this->featureContext->getAdminPassword();
-		}
+		$credentials = $this->getAdminOrUserCredentials($user);
+
 		return GraphHelper::createGroup(
 			$this->featureContext->getBaseUrl(),
 			$this->featureContext->getStepLineRef(),
-			$username,
-			$password,
+			$credentials["username"],
+			$credentials["password"],
 			$group,
 		);
 	}
@@ -615,4 +711,44 @@ class GraphContext implements Context {
 		);
 		$this->featureContext->setResponse($response);
 	}
+
+	/**
+	 * @When user :user gets all the groups using the Graph API
+	 *
+	 * @param string $user
+	 *
+	 * @return void
+	 */
+	public function userGetsAllTheGroupsUsingTheGraphApi(string $user): void {
+		$this->featureContext->setResponse($this->listGroups($user));
+	}
+
+	/**
+	 * @When user :user gets all the members of group :group using the Graph API
+	 *
+	 * @param string $user
+	 * @param string $group
+	 *
+	 * @return void
+	 */
+	public function userGetsAllTheMembersOfGroupUsingTheGraphApi($user, $group): void {
+		$this->featureContext->setResponse($this->listGroupMembers($group, $user));
+	}
+
+	/**
+	 * @Then the last response should be an unauthorized response
+	 *
+	 * @return void
+	 */
+	public function theLastResponseShouldBeUnauthorizedReponse(): void {
+		$response = $this->featureContext->getJsonDecodedResponse($this->featureContext->getResponse());
+		$errorText = $response['error']['message'];
+
+		Assert::assertEquals(
+			'Unauthorized',
+			$errorText,
+			__METHOD__
+			. "\nExpected unauthorized message but got '" . $errorText . "'"
+		);
+	}
 }