Repository: owncloud/ocis
Pull request #2978 (2022-01-18 11:14:39)

add GetSingleDrive handler
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description

Add `GET https://ocis.test/graph/v1.0/drives/{driveId}`

## Motivation and Context
Give the clients the possibility to lookup a single space by ID

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
- added acceptance test for personal space
- added acceptance test for a project space

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [ ] Unit tests added
- [x] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; ![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&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=2978&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&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=2978&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=2978&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&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=2978&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=2978&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=2978&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=2978&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=2978&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_coverage&view=list)  
[![62.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20plus-16px.png '62.6%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_duplicated_lines_density&view=list) [62.6% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; ![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&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=2978&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=2978&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&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=2978&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=2978&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=2978&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=2978&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=2978&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=2978&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_coverage&view=list)  
[![22.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20plus-16px.png '22.6%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_duplicated_lines_density&view=list) [22.6% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=2978&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/changelog/unreleased/single-space-enpoint.md b/changelog/unreleased/single-space-enpoint.md
new file mode 100644
index 00000000000..c77e6a461ac
--- /dev/null
+++ b/changelog/unreleased/single-space-enpoint.md
@@ -0,0 +1,5 @@
+Enhancement: Add endpoint to retrieve a single space
+
+We added the endpoint ``/drives/{driveID}`` to get a single space by id from the server.
+
+https://github.com/owncloud/ocis/pull/2978
diff --git a/deployments/examples/ocis_keycloak/docker-compose.yml b/deployments/examples/ocis_keycloak/docker-compose.yml
index 72be457ae4e..ec11d218f90 100644
--- a/deployments/examples/ocis_keycloak/docker-compose.yml
+++ b/deployments/examples/ocis_keycloak/docker-compose.yml
@@ -63,7 +63,7 @@ services:
       OCIS_URL: https://${OCIS_DOMAIN:-ocis.owncloud.test}
       OCIS_LOG_LEVEL: ${OCIS_LOG_LEVEL:-error} # make oCIS less verbose
       PROXY_TLS: "false" # do not use SSL between Traefik and oCIS
-      ACCOUNTS_DEMO_USERS_AND_GROUPS: "false" # don't generate demo users
+      ACCOUNTS_DEMO_USERS_AND_GROUPS: false # don't generate demo users
       # change default secrets
       IDP_LDAP_BIND_PASSWORD: ${IDP_LDAP_BIND_PASSWORD:-idp}
       STORAGE_LDAP_BIND_PASSWORD: ${STORAGE_LDAP_BIND_PASSWORD:-reva}
diff --git a/graph/pkg/service/v0/drives.go b/graph/pkg/service/v0/drives.go
index 0d15adc48df..2757c8cd982 100644
--- a/graph/pkg/service/v0/drives.go
+++ b/graph/pkg/service/v0/drives.go
@@ -43,46 +43,20 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
 		return
 	}
-	g.logger.Info().Msg("Calling GetDrives")
+	g.logger.Info().Interface("query", r.URL.Query()).Msg("Calling GetDrives")
 	ctx := r.Context()
 
-	client := g.GetGatewayClient()
-
-	permissions := make(map[string]struct{}, 1)
-	s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient)
-
-	_, err = s.GetPermissionByID(ctx, &sproto.GetPermissionByIDRequest{
-		PermissionId: settingsSvc.ListAllSpacesPermissionID,
-	})
-
-	// No error means the user has the permission
-	if err == nil {
-		permissions[settingsSvc.ListAllSpacesPermissionName] = struct{}{}
-	}
-	value, err := json.Marshal(permissions)
-	if err != nil {
-		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
-		return
-	}
 	filters, err := generateCs3Filters(odataReq)
 	if err != nil {
 		g.logger.Err(err).Interface("query", r.URL.Query()).Msg("query error")
 		errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, err.Error())
 		return
 	}
-	res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
-		Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{
-			"permissions": {
-				Decoder: "json",
-				Value:   value,
-			},
-		}},
-		Filters: filters,
-	})
+	res, err := g.ListStorageSpacesWithFilters(ctx, filters)
 	switch {
 	case err != nil:
-		g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request")
-		errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
+		g.logger.Error().Err(err).Msg(ListStorageSpacesTransportErr)
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
 	case res.Status.Code != cs3rpc.Code_CODE_OK:
 		if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
@@ -91,7 +65,7 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
 			render.JSON(w, r, &listResponse{})
 			return
 		}
-		g.logger.Error().Err(err).Msg("error sending list storage spaces grpc request")
+		g.logger.Error().Err(err).Msg(ListStorageSpacesReturnsErr)
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
 		return
 	}
@@ -113,6 +87,75 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
 	render.JSON(w, r, &listResponse{Value: files})
 }
 
+// GetSingleDrive does a lookup of a single space by spaceId
+func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
+	driveID := chi.URLParam(r, "driveID")
+	if driveID == "" {
+		err := fmt.Errorf("no valid space id retrieved")
+		g.logger.Err(err)
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	g.logger.Info().Str("driveID", driveID).Msg("Calling GetSingleDrive")
+	ctx := r.Context()
+
+	filters := []*storageprovider.ListStorageSpacesRequest_Filter{
+		{
+			Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_ID,
+			Term: &storageprovider.ListStorageSpacesRequest_Filter_Id{
+				Id: &storageprovider.StorageSpaceId{
+					OpaqueId: driveID,
+				},
+			},
+		},
+	}
+	res, err := g.ListStorageSpacesWithFilters(ctx, filters)
+	switch {
+	case err != nil:
+		g.logger.Error().Err(err).Msg(ListStorageSpacesTransportErr)
+		errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	case res.Status.Code != cs3rpc.Code_CODE_OK:
+		if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
+			// the client is doing a lookup for a specific space, therefore we need to return
+			// not found to the caller
+			g.logger.Error().Str("driveID", driveID).Msg(fmt.Sprintf(NoSpaceFoundMessage, driveID))
+			errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf(NoSpaceFoundMessage, driveID))
+			return
+		}
+		g.logger.Error().Err(err).Msg(ListStorageSpacesReturnsErr)
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
+		return
+	}
+
+	wdu, err := url.Parse(g.config.Spaces.WebDavBase + g.config.Spaces.WebDavPath)
+	if err != nil {
+		g.logger.Error().Err(err).Msg("error parsing url")
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+	spaces, err := g.formatDrives(ctx, wdu, res.StorageSpaces)
+	if err != nil {
+		g.logger.Error().Err(err).Msg("error encoding response")
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+	switch num := len(spaces); {
+	case num == 0:
+		g.logger.Error().Str("driveID", driveID).Msg("no space found")
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf(NoSpaceFoundMessage, driveID))
+		return
+	case num == 1:
+		render.Status(r, http.StatusOK)
+		render.JSON(w, r, spaces[0])
+	default:
+		g.logger.Error().Int("number", num).Msg("expected to find a single space but found more")
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "expected to find a single space but found more")
+		return
+	}
+}
+
 // CreateDrive creates a storage drive (space).
 func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
 	us, ok := ctxpkg.ContextGetUser(r.Context())
@@ -340,6 +383,38 @@ func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storag
 	return responses, nil
 }
 
+// ListStorageSpacesWithFilters List Storage Spaces using filters
+func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*storageprovider.ListStorageSpacesRequest_Filter) (*storageprovider.ListStorageSpacesResponse, error) {
+	client := g.GetGatewayClient()
+
+	permissions := make(map[string]struct{}, 1)
+	s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient)
+
+	_, err := s.GetPermissionByID(ctx, &sproto.GetPermissionByIDRequest{
+		PermissionId: settingsSvc.ListAllSpacesPermissionID,
+	})
+
+	// No error means the user has the permission
+	if err == nil {
+		permissions[settingsSvc.ListAllSpacesPermissionName] = struct{}{}
+	}
+	value, err := json.Marshal(permissions)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
+		Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{
+			"permissions": {
+				Decoder: "json",
+				Value:   value,
+			},
+		}},
+		Filters: filters,
+	})
+	return res, err
+}
+
 func cs3StorageSpaceToDrive(baseURL *url.URL, space *storageprovider.StorageSpace) (*libregraph.Drive, error) {
 	rootID := space.Root.StorageId + "!" + space.Root.OpaqueId
 	if space.Root.StorageId == space.Root.OpaqueId {
diff --git a/graph/pkg/service/v0/graph.go b/graph/pkg/service/v0/graph.go
index 3523dd8fb5e..8117859194e 100644
--- a/graph/pkg/service/v0/graph.go
+++ b/graph/pkg/service/v0/graph.go
@@ -85,3 +85,9 @@ func (g Graph) GetHTTPClient() HTTPClient {
 type listResponse struct {
 	Value interface{} `json:"value,omitempty"`
 }
+
+const (
+	NoSpaceFoundMessage           = "space with id `%s` not found"
+	ListStorageSpacesTransportErr = "transport error sending list storage spaces grpc request"
+	ListStorageSpacesReturnsErr   = "list storage spaces grpc request returns an errorcode in the response"
+)
diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go
index 5d62e3cdb74..529fec816b3 100644
--- a/graph/pkg/service/v0/service.go
+++ b/graph/pkg/service/v0/service.go
@@ -117,6 +117,7 @@ func NewService(opts ...Option) Service {
 					r.Post("/", svc.CreateDrive)
 					r.Route("/{driveID}", func(r chi.Router) {
 						r.Patch("/", svc.UpdateDrive)
+						r.Get("/", svc.GetSingleDrive)
 					})
 				})
 			})


Test patch:
diff --git a/tests/acceptance/features/apiSpaces/listSpaces.feature b/tests/acceptance/features/apiSpaces/listSpaces.feature
index 175c562cc77..ac3afc65e90 100644
--- a/tests/acceptance/features/apiSpaces/listSpaces.feature
+++ b/tests/acceptance/features/apiSpaces/listSpaces.feature
@@ -79,3 +79,35 @@ Feature: List and create spaces
       | name             | Project Venus |
       | quota@@@total    | 2000          |
       | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
+
+  Scenario: A user can list his personal space via multiple endpoints
+    When user "Alice" lists all available spaces via the GraphApi with query "$filter=driveType eq 'personal'"
+    Then the json responded should contain a space "Alice Hansen" with these key and value pairs:
+      | key              | value         |
+      | driveType        | personal      |
+      | name             | Alice Hansen  |
+      | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
+    When user "Alice" looks up the single space "Alice Hansen" via the GraphApi by using its id
+    Then the json responded should contain a space "Alice Hansen" with these key and value pairs:
+      | key              | value         |
+      | driveType        | personal      |
+      | name             | Alice Hansen  |
+      | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
+
+  Scenario: A user can list his created spaces via multiple endpoints
+    Given the administrator has given "Alice" the role "Admin" using the settings api
+    When user "Alice" creates a space "Project Venus" of type "project" with quota "2000" using the GraphApi
+    Then the HTTP status code should be "201"
+    And the json responded should contain a space "Project Venus" with these key and value pairs:
+      | key              | value         |
+      | driveType        | project       |
+      | name             | Project Venus |
+      | quota@@@total    | 2000          |
+      | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
+    When user "Alice" looks up the single space "Project Venus" via the GraphApi by using its id
+    Then the json responded should contain a space "Project Venus" with these key and value pairs:
+      | key              | value         |
+      | driveType        | project       |
+      | name             | Project Venus |
+      | quota@@@total    | 2000          |
+      | root@@@webDavUrl | %base_url%/dav/spaces/%space_id% |
diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php
index c5d5a220ae5..b0e03b2ef8c 100644
--- a/tests/acceptance/features/bootstrap/SpacesContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesContext.php
@@ -232,7 +232,7 @@ class SpacesContext implements Context {
 	}
 
 	/**
-	 * Send Graph List Spaces Request
+	 * Send Graph List My Spaces Request
 	 *
 	 * @param  string $user
 	 * @param  string $password
@@ -245,7 +245,7 @@ class SpacesContext implements Context {
 	 *
 	 * @throws GuzzleException
 	 */
-	public function listSpacesRequest(
+	public function listMySpacesRequest(
 		string $user,
 		string $password,
 		string $urlArguments = '',
@@ -258,6 +258,34 @@ class SpacesContext implements Context {
 		return HttpRequestHelper::get($fullUrl, $xRequestId, $user, $password, $headers, $body);
 	}
 
+	/**
+	 * Send Graph List Single Space Request
+	 *
+	 * @param  string $user
+	 * @param  string $password
+	 * @param  string $urlArguments
+	 * @param  string $xRequestId
+	 * @param  array  $body
+	 * @param  array  $headers
+	 *
+	 * @return ResponseInterface
+	 *
+	 * @throws GuzzleException
+	 */
+	public function listSingleSpaceRequest(
+		string $user,
+		string $password,
+		string $spaceId,
+		string $urlArguments = '',
+		string $xRequestId = '',
+		array  $body = [],
+		array  $headers = []
+	): ResponseInterface {
+		$fullUrl = $this->baseUrl . "/graph/v1.0/drives/" . $spaceId . "/" . $urlArguments;
+
+		return HttpRequestHelper::get($fullUrl, $xRequestId, $user, $password, $headers, $body);
+	}
+
 	/**
 	 * Send Graph Create Space Request
 	 *
@@ -342,7 +370,7 @@ class SpacesContext implements Context {
 	 */
 	public function theUserListsAllHisAvailableSpacesUsingTheGraphApi(string $user): void {
 		$this->featureContext->setResponse(
-			$this->listSpacesRequest(
+			$this->listMySpacesRequest(
 				$user,
 				$this->featureContext->getPasswordForUser($user)
 			)
@@ -362,7 +390,7 @@ class SpacesContext implements Context {
 	 */
 	public function theUserListsAllHisAvailableSpacesUsingTheGraphApiWithFilter(string $user, string $query): void {
 		$this->featureContext->setResponse(
-			$this->listSpacesRequest(
+			$this->listMySpacesRequest(
 				$user,
 				$this->featureContext->getPasswordForUser($user),
 				"?". $query
@@ -370,6 +398,30 @@ class SpacesContext implements Context {
 		);
 	}
 
+	/**
+	 * @When /^user "([^"]*)" looks up the single space "([^"]*)" via the GraphApi by using its id$/
+	 *
+	 * @param string $user
+	 * @param string $query
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 */
+	public function theUserLooksUpTheSingleSpaceUsingTheGraphApiByUsingItsId(string $user, string $spaceName): void {
+		$space = $this->getSpaceByName($user, $spaceName);
+		Assert::assertIsArray($space);
+		Assert::assertNotEmpty($spaceId = $space["id"]);
+		Assert::assertNotEmpty($spaceWebDavUrl = $space["root"]["webDavUrl"]);
+		$this->featureContext->setResponse(
+			$this->listSingleSpaceRequest(
+				$user,
+				$this->featureContext->getPasswordForUser($user),
+				$spaceId
+			)
+		);
+	}
+
 	/**
 	 * @When /^user "([^"]*)" creates a space "([^"]*)" of type "([^"]*)" with the default quota using the GraphApi$/
 	 *