From 8f5dc2ebaf5391450b92ff3feb7a93ae2082d787 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Tue, 10 Nov 2026 13:13:21 +0100
Subject: [PATCH] feat(graph): add the v1beta1 endpoints for a single drive
Under /graph/v1beta1, drives could only be listed. Getting, creating,
updating and deleting a single drive was only possible under v1.0,
which returns the permissions of a drive's root with the old role names
instead of the IDs of the unified roles.
Add GET, PATCH and DELETE on /graph/v1beta1/drives/{driveID}, and POST
on /graph/v1beta1/drives. They take the same requests as their v1.0
counterparts. GET, POST and PATCH return the drive in the v1beta1
format, like the v1beta1 list of drives does: with
$expand=root($expand=permissions), the permissions carry the unified
role IDs and grantedToV2. DELETE behaves the same in both versions.
The handlers for getting, creating and updating a drive now take the
API version as a parameter, and the v1.0 and v1beta1 routes call them
with their own version.
The acceptance test helpers for spaces can now use either version of
the Graph API.
Assisted-by: Claude Code:claude-opus-5-5
---
services/graph/pkg/service/v0/drives.go | 44 +++++-
services/graph/pkg/service/v0/graph_test.go | 70 +++++++-
services/graph/pkg/service/v0/service.go | 7 +
tests/acceptance/TestHelpers/GraphHelper.php | 49 ++++--
tests/acceptance/bootstrap/SpacesContext.php | 149 +++++++++++++++++-
.../apiSpaces/spaceApiVersions.feature | 84 ++++++++++
6 files changed, 371 insertions(+), 32 deletions(-)
create mode 100644 tests/acceptance/features/apiSpaces/spaceApiVersions.feature
diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go
index 2351e4a6af..35c7d35882 100644
--- a/services/graph/pkg/service/v0/drives.go
+++ b/services/graph/pkg/service/v0/drives.go
@@ -270,8 +270,19 @@ func (g Graph) getDrives(r *http.Request, unrestricted bool, apiVersion APIVersi
return spaces, nil
}
-// GetSingleDrive does a lookup of a single space by spaceId
+// GetSingleDrive returns the space with the given ID, in the v1.0 format.
func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
+ g.getSingleDrive(w, r, APIVersion_1)
+}
+
+// GetSingleDriveV1Beta1 returns the space with the given ID, in the v1beta1
+// format: its expanded root permissions name unified roles.
+func (g Graph) GetSingleDriveV1Beta1(w http.ResponseWriter, r *http.Request) {
+ g.getSingleDrive(w, r, APIVersion_1_Beta_1)
+}
+
+// getSingleDrive returns the space with the given ID, in the format of the given API version.
+func (g Graph) getSingleDrive(w http.ResponseWriter, r *http.Request, apiVersion APIVersion) {
ctx := r.Context()
log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling get drive")
@@ -326,7 +337,7 @@ func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
return
}
- spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, APIVersion_1, expandPermissions, getFieldMask(odataReq))
+ spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, apiVersion, expandPermissions, getFieldMask(odataReq))
if err != nil {
log.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -361,8 +372,18 @@ func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
return true
}
-// CreateDrive creates a storage drive (space).
+// CreateDrive creates a space and returns it in the v1.0 format.
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
+ g.createDrive(w, r, APIVersion_1)
+}
+
+// CreateDriveV1Beta1 creates a space and returns it in the v1beta1 format.
+func (g Graph) CreateDriveV1Beta1(w http.ResponseWriter, r *http.Request) {
+ g.createDrive(w, r, APIVersion_1_Beta_1)
+}
+
+// createDrive creates a space and returns it in the format of the given API version.
+func (g Graph) createDrive(w http.ResponseWriter, r *http.Request, apiVersion APIVersion) {
ctx := r.Context()
log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling create drive")
@@ -515,7 +536,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
}
}
- spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1, false, nil)
+ spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, apiVersion, false, nil)
if err != nil {
log.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -531,8 +552,19 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, spaces[0])
}
-// UpdateDrive updates the properties of a storage drive (space).
+// UpdateDrive updates the properties of a space and returns it in the v1.0 format.
func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
+ g.updateDrive(w, r, APIVersion_1)
+}
+
+// UpdateDriveV1Beta1 updates the properties of a space and returns it in the
+// v1beta1 format.
+func (g Graph) UpdateDriveV1Beta1(w http.ResponseWriter, r *http.Request) {
+ g.updateDrive(w, r, APIVersion_1_Beta_1)
+}
+
+// updateDrive updates the properties of a space and returns it in the format of the given API version.
+func (g Graph) updateDrive(w http.ResponseWriter, r *http.Request, apiVersion APIVersion) {
log := g.logger.SubloggerWithRequestID(r.Context()).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling update drive")
@@ -687,7 +719,7 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
}
}
- spaces, err := g.formatDrives(r.Context(), webDavBaseURL, []*storageprovider.StorageSpace{resp.StorageSpace}, APIVersion_1, false, nil)
+ spaces, err := g.formatDrives(r.Context(), webDavBaseURL, []*storageprovider.StorageSpace{resp.StorageSpace}, apiVersion, false, nil)
if err != nil {
log.Debug().Err(err).Msg("could not update drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
diff --git a/services/graph/pkg/service/v0/graph_test.go b/services/graph/pkg/service/v0/graph_test.go
index 8d0bff30a2..78ee646a8a 100644
--- a/services/graph/pkg/service/v0/graph_test.go
+++ b/services/graph/pkg/service/v0/graph_test.go
@@ -843,7 +843,7 @@ var _ = Describe("Graph", func() {
Expect(libreError.Error.Message).To(Equal("invalid spacename: spacenames must not contain [/ \\ . : ? * \" > < |]"))
Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
})
- It("can create a project space", func() {
+ DescribeTable("can create a project space", func(path string, createDrive func(service.Service, http.ResponseWriter, *http.Request)) {
gatewayClient.On("CreateStorageSpace", mock.Anything, mock.Anything).Return(&provider.CreateStorageSpaceResponse{
Status: status.NewOK(ctx),
StorageSpace: &provider.StorageSpace{
@@ -878,9 +878,9 @@ var _ = Describe("Graph", func() {
}, nil)
jsonBody := []byte(`{"name": "Test Space", "driveType": "project", "description": "This space is for testing", "DriveAlias": "project/testspace", "@libre.graph.contentType": "application/vnd.opencloud.test"}`)
- r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+ r := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(jsonBody)).WithContext(ctx)
rr := httptest.NewRecorder()
- svc.CreateDrive(rr, r)
+ createDrive(svc, rr, r)
Expect(rr.Code).To(Equal(http.StatusCreated))
body, _ := io.ReadAll(rr.Body)
@@ -892,7 +892,10 @@ var _ = Describe("Graph", func() {
Expect(*response.DriveAlias).To(Equal("project/testspace"))
Expect(*response.Description).To(Equal("This space is for testing"))
Expect(*response.LibreGraphContentType).To(Equal("application/vnd.opencloud.test"))
- })
+ },
+ Entry("with v1.0", "/graph/v1.0/drives", service.Service.CreateDrive),
+ Entry("with v1beta1", "/graph/v1beta1/drives", service.Service.CreateDriveV1Beta1),
+ )
Context("drive id handling", func() {
const spaceID = "0b9a7b84-3e71-4a4e-9c34-0e6a4c3f5f21"
@@ -1235,6 +1238,54 @@ var _ = Describe("Graph", func() {
Expect(*drive.GetQuota().Total).To(Equal(int64(500)))
})
+ DescribeTable("returns the expanded root permissions in the format of the API version",
+ func(path string, getSingleDrive func(service.Service, http.ResponseWriter, *http.Request), check func(permission gjson.Result)) {
+ gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+ Status: status.NewOK(ctx),
+ StorageSpaces: []*provider.StorageSpace{
+ {
+ Opaque: utils.AppendJSONToOpaque(nil, "grants", map[string]provider.ResourcePermissions{
+ "1": *conversions.NewManagerRole().CS3ResourcePermissions(),
+ }),
+ Id: &provider.StorageSpaceId{OpaqueId: "spaceid"},
+ SpaceType: "project",
+ Root: &provider.ResourceId{
+ StorageId: "pro-1",
+ SpaceId: "spaceid",
+ OpaqueId: "spaceid",
+ },
+ Name: "aspacename",
+ },
+ },
+ }, nil)
+ gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&userprovider.GetUserResponse{
+ Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
+ }, nil)
+
+ r := httptest.NewRequest(http.MethodGet, path+"?$expand=root($expand=permissions)", nil)
+ rctx := chi.NewRouteContext()
+ rctx.URLParams.Add("driveID", "spaceid")
+ r = r.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx))
+ getSingleDrive(svc, rr, r)
+ Expect(rr.Code).To(Equal(http.StatusOK))
+
+ permissions := gjson.Get(rr.Body.String(), "root.permissions")
+ Expect(permissions.Get("#").Int()).To(Equal(int64(1)))
+ Expect(permissions.Get("0.grantedToV2.user.id").Str).To(Equal("1"))
+ check(permissions.Get("0"))
+ },
+ Entry("with v1.0: the old role name", "/graph/v1.0/drives/spaceid", service.Service.GetSingleDrive,
+ func(permission gjson.Result) {
+ Expect(permission.Get("roles").Array()).To(ConsistOf(HaveField("Str", "manager")))
+ Expect(permission.Get("grantedToIdentities").Exists()).To(BeTrue())
+ }),
+ Entry("with v1beta1: the ID of the unified role", "/graph/v1beta1/drives/spaceid", service.Service.GetSingleDriveV1Beta1,
+ func(permission gjson.Result) {
+ Expect(permission.Get("roles").Array()).To(ConsistOf(HaveField("Str", unifiedrole.UnifiedRoleManagerID)))
+ Expect(permission.Get("grantedToIdentities").Exists()).To(BeFalse())
+ }),
+ )
+
It("gets the special drive items", func() {
gatewayClient.On("GetPath", mock.Anything, mock.Anything).Return(&provider.GetPathResponse{
Status: status.NewOK(ctx),
@@ -1343,7 +1394,7 @@ var _ = Describe("Graph", func() {
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
- It("sets the description, alias, name and content type", func() {
+ DescribeTable("sets the description, alias, name and content type", func(path string, updateDrive func(service.Service, http.ResponseWriter, *http.Request)) {
drive := libregraph.NewDrive("thename")
drive.SetDriveAlias("thealias")
drive.SetDescription("thedescription")
@@ -1362,11 +1413,11 @@ var _ = Describe("Graph", func() {
Status: status.NewNotFound(ctx, "no special files found"),
}, nil)
- r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/drives/{driveID}/", bytes.NewBuffer(driveJson))
+ r := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(driveJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "spaceid")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
- svc.UpdateDrive(rr, r)
+ updateDrive(svc, rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
gatewayClient.AssertCalled(GinkgoT(), "UpdateStorageSpace", mock.Anything, mock.MatchedBy(func(req *provider.UpdateStorageSpaceRequest) bool {
@@ -1375,7 +1426,10 @@ var _ = Describe("Graph", func() {
utils.ReadPlainFromOpaque(req.StorageSpace.Opaque, "spaceAlias") == drive.GetDriveAlias() &&
utils.ReadPlainFromOpaque(req.StorageSpace.Opaque, "contentType") == drive.GetLibreGraphContentType()
}))
- })
+ },
+ Entry("with v1.0", "/graph/v1.0/drives/spaceid", service.Service.UpdateDrive),
+ Entry("with v1beta1", "/graph/v1beta1/drives/spaceid", service.Service.UpdateDriveV1Beta1),
+ )
DescribeTable("rejects an invalid name without updating the space",
func(name string) {
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index 9dd651f346..4a2468cac1 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -94,10 +94,13 @@ type Service interface { //nolint:interfacebloat
GetDrivesV1(w http.ResponseWriter, r *http.Request)
GetDrivesV1Beta1(w http.ResponseWriter, r *http.Request)
GetSingleDrive(w http.ResponseWriter, r *http.Request)
+ GetSingleDriveV1Beta1(w http.ResponseWriter, r *http.Request)
GetAllDrivesV1(w http.ResponseWriter, r *http.Request)
GetAllDrivesV1Beta1(w http.ResponseWriter, r *http.Request)
CreateDrive(w http.ResponseWriter, r *http.Request)
+ CreateDriveV1Beta1(w http.ResponseWriter, r *http.Request)
UpdateDrive(w http.ResponseWriter, r *http.Request)
+ UpdateDriveV1Beta1(w http.ResponseWriter, r *http.Request)
DeleteDrive(w http.ResponseWriter, r *http.Request)
GetSharedByMe(w http.ResponseWriter, r *http.Request)
@@ -250,6 +253,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
})
r.Route("/drives", func(r chi.Router) {
r.Get("/", svc.GetAllDrives(APIVersion_1_Beta_1))
+ r.Post("/", svc.CreateDriveV1Beta1)
r.Route("/{driveID}", func(r chi.Router) {
// Rewrites MS Graph colon-syntax lookups (root:/path,
// items/{id}:/path) to /items/{resolvedID}... before chi
@@ -257,6 +261,9 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
// /drives/{driveID} sub-router, so it can rewrite the
// remaining RoutePath. See graphm.ResolveGraphPath.
r.Use(graphm.ResolveGraphPath(options.GatewaySelector, options.Logger))
+ r.Get("/", svc.GetSingleDriveV1Beta1)
+ r.Patch("/", svc.UpdateDriveV1Beta1)
+ r.Delete("/", svc.DeleteDrive)
r.Route("/root", func(r chi.Router) {
r.Post("/children", drivesDriveItemApi.CreateDriveItem)
r.Post("/invite", driveItemPermissionsApi.SpaceRootInvite)
diff --git a/tests/acceptance/TestHelpers/GraphHelper.php b/tests/acceptance/TestHelpers/GraphHelper.php
index 052dcf699d..50cbbde741 100644
--- a/tests/acceptance/TestHelpers/GraphHelper.php
+++ b/tests/acceptance/TestHelpers/GraphHelper.php
@@ -21,6 +21,7 @@
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
+use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
@@ -207,6 +208,24 @@ public static function getBetaFullUrl(string $baseUrl, string $path): string {
return $baseUrl . '/graph/v1beta1/' . $path;
}
+ /**
+ * the URL of a path of the Graph API, in the given version of the API
+ *
+ * @param string $baseUrl
+ * @param string $apiVersion "v1.0" or "v1beta1"
+ * @param string $path
+ *
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public static function getFullUrlForApiVersion(string $baseUrl, string $apiVersion, string $path): string {
+ return match ($apiVersion) {
+ 'v1.0' => self::getFullUrl($baseUrl, $path),
+ 'v1beta1' => self::getBetaFullUrl($baseUrl, $path),
+ default => throw new InvalidArgumentException("unknown Graph API version '$apiVersion'"),
+ };
+ }
+
/**
* @param string $baseUrl
* @param string $xRequestId
@@ -953,6 +972,7 @@ public static function preparePatchUserPayload(
* @param string $body
* @param string $xRequestId
* @param array $headers
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -963,9 +983,10 @@ public static function createSpace(
string $password,
string $body,
string $xRequestId = '',
- array $headers = []
+ array $headers = [],
+ string $apiVersion = 'v1.0'
): ResponseInterface {
- $url = self::getFullUrl($baseUrl, 'drives');
+ $url = self::getFullUrlForApiVersion($baseUrl, $apiVersion, 'drives');
return HttpRequestHelper::post($url, $xRequestId, $user, $password, $headers, $body);
}
@@ -980,6 +1001,7 @@ public static function createSpace(
* @param string $spaceId
* @param string $xRequestId
* @param array $headers
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -991,9 +1013,10 @@ public static function updateSpace(
$body,
string $spaceId,
string $xRequestId = '',
- array $headers = []
+ array $headers = [],
+ string $apiVersion = 'v1.0'
): ResponseInterface {
- $url = self::getFullUrl($baseUrl, 'drives/' . $spaceId);
+ $url = self::getFullUrlForApiVersion($baseUrl, $apiVersion, 'drives/' . $spaceId);
return HttpRequestHelper::sendRequest($url, $xRequestId, 'PATCH', $user, $password, $headers, $body);
}
@@ -1067,6 +1090,7 @@ public static function getAllSpaces(
* @param string $xRequestId
* @param array $body
* @param array $headers
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
*
@@ -1080,9 +1104,10 @@ public static function getSingleSpace(
string $urlArguments = '',
string $xRequestId = '',
array $body = [],
- array $headers = []
+ array $headers = [],
+ string $apiVersion = 'v1.0'
): ResponseInterface {
- $url = self::getFullUrl($baseUrl, 'drives/' . $spaceId . "/" . $urlArguments);
+ $url = self::getFullUrlForApiVersion($baseUrl, $apiVersion, 'drives/' . $spaceId . "/" . $urlArguments);
return HttpRequestHelper::get($url, $xRequestId, $user, $password, $headers, $body);
}
@@ -1095,6 +1120,7 @@ public static function getSingleSpace(
* @param string $password
* @param string $spaceId
* @param string $xRequestId
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -1104,9 +1130,10 @@ public static function disableSpace(
string $user,
string $password,
string $spaceId,
- string $xRequestId = ''
+ string $xRequestId = '',
+ string $apiVersion = 'v1.0'
): ResponseInterface {
- $url = self::getFullUrl($baseUrl, 'drives/' . $spaceId);
+ $url = self::getFullUrlForApiVersion($baseUrl, $apiVersion, 'drives/' . $spaceId);
return HttpRequestHelper::delete(
$url,
@@ -1124,6 +1151,7 @@ public static function disableSpace(
* @param string $password
* @param string $spaceId
* @param string $xRequestId
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -1133,9 +1161,10 @@ public static function deleteSpace(
string $user,
string $password,
string $spaceId,
- string $xRequestId = ''
+ string $xRequestId = '',
+ string $apiVersion = 'v1.0'
): ResponseInterface {
- $url = self::getFullUrl($baseUrl, 'drives/' . $spaceId);
+ $url = self::getFullUrlForApiVersion($baseUrl, $apiVersion, 'drives/' . $spaceId);
$header = ["Purge" => "T"];
return HttpRequestHelper::delete(
diff --git a/tests/acceptance/bootstrap/SpacesContext.php b/tests/acceptance/bootstrap/SpacesContext.php
index 3af2560fbe..9ff0d6ce18 100644
--- a/tests/acceptance/bootstrap/SpacesContext.php
+++ b/tests/acceptance/bootstrap/SpacesContext.php
@@ -778,6 +778,125 @@ public function theUserLooksUpTheSingleSpaceUsingTheGraphApiByUsingItsId(
);
}
+ /**
+ * @param string $user
+ * @param string $spaceName
+ * @param string $apiVersion "v1.0" or "v1beta1"
+ * @param string $query
+ *
+ * @return void
+ * @throws GuzzleException
+ */
+ #[When('user :user looks up the single space :spaceName using the Graph API version :apiVersion')]
+ #[When('user :user looks up the single space :spaceName using the Graph API version :apiVersion with query :query')]
+ public function theUserLooksUpTheSingleSpaceUsingTheGraphApiVersion(
+ string $user,
+ string $spaceName,
+ string $apiVersion,
+ string $query = ''
+ ): void {
+ $space = $this->getSpaceByName($user, $spaceName);
+ Assert::assertNotEmpty($space["id"]);
+ $this->featureContext->setResponse(
+ GraphHelper::getSingleSpace(
+ $this->featureContext->getBaseUrl(),
+ $user,
+ $this->featureContext->getPasswordForUser($user),
+ $space["id"],
+ $query !== '' ? '?' . $query : '',
+ $this->featureContext->getStepLineRef(),
+ [],
+ [],
+ $apiVersion
+ )
+ );
+ }
+
+ /**
+ * @param string $user
+ * @param string $spaceName
+ * @param string $spaceType
+ * @param string $quota
+ * @param string $apiVersion "v1.0" or "v1beta1"
+ *
+ * @return void
+ * @throws GuzzleException
+ * @throws JsonException
+ */
+ #[When('user :user creates a space :spaceName of type :spaceType with quota :quota using the Graph API version :apiVersion')]
+ public function theUserCreatesASpaceUsingTheGraphApiVersion(
+ string $user,
+ string $spaceName,
+ string $spaceType,
+ string $quota,
+ string $apiVersion
+ ): void {
+ $space = ["name" => $spaceName, "driveType" => $spaceType, "quota" => ["total" => (int)$quota]];
+ $this->featureContext->setResponse(
+ $this->createSpace($user, $space, '', $apiVersion)
+ );
+ }
+
+ /**
+ * @param string $user
+ * @param string $spaceName
+ * @param string $newName
+ * @param string $apiVersion "v1.0" or "v1beta1"
+ *
+ * @return void
+ * @throws GuzzleException
+ * @throws JsonException
+ */
+ #[When('user :user changes the name of the :spaceName space to :newName using the Graph API version :apiVersion')]
+ public function updateSpaceNameUsingTheGraphApiVersion(
+ string $user,
+ string $spaceName,
+ string $newName,
+ string $apiVersion
+ ): void {
+ $this->featureContext->setResponse(
+ $this->updateSpace($user, $spaceName, ["Name" => $newName], '', $apiVersion)
+ );
+ }
+
+ /**
+ * @param string $user
+ * @param string $spaceName
+ * @param string $apiVersion "v1.0" or "v1beta1"
+ *
+ * @return void
+ * @throws GuzzleException
+ */
+ #[When('user :user disables a space :spaceName using the Graph API version :apiVersion')]
+ public function userDisablesSpaceUsingTheGraphApiVersion(
+ string $user,
+ string $spaceName,
+ string $apiVersion
+ ): void {
+ $this->featureContext->setResponse(
+ $this->disableSpace($user, $spaceName, '', $apiVersion)
+ );
+ }
+
+ /**
+ * @param string $user
+ * @param string $spaceName
+ * @param string $apiVersion "v1.0" or "v1beta1"
+ *
+ * @return void
+ * @throws GuzzleException
+ */
+ #[When('user :user deletes a space :spaceName using the Graph API version :apiVersion')]
+ public function userDeletesSpaceUsingTheGraphApiVersion(
+ string $user,
+ string $spaceName,
+ string $apiVersion
+ ): void {
+ $this->featureContext->setResponse(
+ $this->deleteSpace($user, $spaceName, '', $apiVersion)
+ );
+ }
+
/**
*
* @param string $user
@@ -1578,6 +1697,7 @@ public function theUserUploadsAFileToAnotherOwnerSpace(
* @param string $spaceName
* @param array $bodyData
* @param string $owner
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -1587,7 +1707,8 @@ public function updateSpace(
string $user,
string $spaceName,
array $bodyData,
- string $owner = ''
+ string $owner = '',
+ string $apiVersion = 'v1.0'
): ResponseInterface {
if ($spaceName === "non-existing") {
// check sending invalid data
@@ -1605,7 +1726,9 @@ public function updateSpace(
$this->featureContext->getPasswordForUser($user),
$body,
$spaceId,
- $this->featureContext->getStepLineRef()
+ $this->featureContext->getStepLineRef(),
+ [],
+ $apiVersion
);
}
@@ -1952,6 +2075,7 @@ public function theUserHasCreatedASpaceByDefaultUsingTheGraphApi(
* @param string $user
* @param array $space
* @param string $body
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -1960,7 +2084,8 @@ public function theUserHasCreatedASpaceByDefaultUsingTheGraphApi(
public function createSpace(
string $user,
array $space,
- string $body = ''
+ string $body = '',
+ string $apiVersion = 'v1.0'
): ResponseInterface {
$body = $body !== '' ? $body : json_encode($space, JSON_THROW_ON_ERROR);
$response = GraphHelper::createSpace(
@@ -1968,7 +2093,9 @@ public function createSpace(
$user,
$this->featureContext->getPasswordForUser($user),
$body,
- $this->featureContext->getStepLineRef()
+ $this->featureContext->getStepLineRef(),
+ [],
+ $apiVersion
);
if ($response->getStatusCode() === 201) {
$this->addCreatedSpace($user, $response);
@@ -3013,6 +3140,7 @@ public function userHasDeletedASpaceOwnedByUser(
* @param string $user
* @param string $spaceName
* @param string $owner
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -3020,7 +3148,8 @@ public function userHasDeletedASpaceOwnedByUser(
public function disableSpace(
string $user,
string $spaceName,
- string $owner = ''
+ string $owner = '',
+ string $apiVersion = 'v1.0'
): ResponseInterface {
$space = $this->getSpaceByName(($owner !== "") ? $owner : $user, $spaceName);
return GraphHelper::disableSpace(
@@ -3028,7 +3157,8 @@ public function disableSpace(
$user,
$this->featureContext->getPasswordForUser($user),
$space["id"],
- $this->featureContext->getStepLineRef()
+ $this->featureContext->getStepLineRef(),
+ $apiVersion
);
}
@@ -3103,6 +3233,7 @@ public function sendUserHasDisabledSpaceRequest(
* @param string $user
* @param string $spaceName
* @param string $owner
+ * @param string $apiVersion "v1.0" or "v1beta1"
*
* @return ResponseInterface
* @throws GuzzleException
@@ -3110,7 +3241,8 @@ public function sendUserHasDisabledSpaceRequest(
public function deleteSpace(
string $user,
string $spaceName,
- string $owner = ''
+ string $owner = '',
+ string $apiVersion = 'v1.0'
): ResponseInterface {
$space = $this->getSpaceByName(($owner !== "") ? $owner : $user, $spaceName);
@@ -3119,7 +3251,8 @@ public function deleteSpace(
$user,
$this->featureContext->getPasswordForUser($user),
$space["id"],
- $this->featureContext->getStepLineRef()
+ $this->featureContext->getStepLineRef(),
+ $apiVersion
);
}
diff --git a/tests/acceptance/features/apiSpaces/spaceApiVersions.feature b/tests/acceptance/features/apiSpaces/spaceApiVersions.feature
new file mode 100644
index 0000000000..901ad17702
--- /dev/null
+++ b/tests/acceptance/features/apiSpaces/spaceApiVersions.feature
@@ -0,0 +1,84 @@
+Feature: manage a space through each version of the Graph API
+ As a space admin
+ I want to create, get, change and delete a space through v1.0 and through v1beta1
+ So that my client can use the version whose format of permissions it understands
+
+ Background:
+ Given user "Alice" has been created with default attributes
+ And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
+
+
+ Scenario Outline: space admin manages a project space using the Graph API version
+ When user "Alice" creates a space "Project Venus" of type "project" with quota "2000" using the Graph API version ""
+ Then the HTTP status code should be "201"
+ And the JSON data of the response should match
+ """
+ {
+ "type": "object",
+ "required": ["name", "driveType"],
+ "properties": {
+ "name": {
+ "const": "Project Venus"
+ },
+ "driveType": {
+ "const": "project"
+ }
+ }
+ }
+ """
+ When user "Alice" looks up the single space "Project Venus" using the Graph API version "" with query "$expand=root($expand=permissions)"
+ Then the HTTP status code should be "200"
+ And the JSON data of the response should match
+ """
+ {
+ "type": "object",
+ "required": ["name", "root"],
+ "properties": {
+ "name": {
+ "const": "Project Venus"
+ },
+ "root": {
+ "type": "object",
+ "required": ["permissions"],
+ "properties": {
+ "permissions": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 1,
+ "items": {
+ "type": "object",
+ "required": ["roles", "grantedToV2"],
+ "properties": {
+ "roles": {
+ "const": [""]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """
+ When user "Alice" changes the name of the "Project Venus" space to "Project Mars" using the Graph API version ""
+ Then the HTTP status code should be "200"
+ And the JSON data of the response should match
+ """
+ {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "const": "Project Mars"
+ }
+ }
+ }
+ """
+ When user "Alice" disables a space "Project Mars" using the Graph API version ""
+ Then the HTTP status code should be "204"
+ When user "Alice" deletes a space "Project Mars" using the Graph API version ""
+ Then the HTTP status code should be "204"
+ Examples:
+ | api-version | manager-role |
+ | v1.0 | manager |
+ | v1beta1 | 312c0871-5ef7-4b3a-85b6-0e4074c64049 |