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 |