From 3b42c6250dbbcbc114b17eeb902b84334ed856ca Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 6 May 2026 12:46:42 +0200 Subject: [PATCH] test(graph): acceptance tests for MS Graph colon-syntax path lookup Cover the rewrite shapes the middleware handles end-to-end against a real OpenCloud server: root-anchored, item-anchored, deep paths, trailing colon, and the "/:/" sub-route form. Also assert that NOT_FOUND and PERMISSION_DENIED both collapse to 404. The /permissions sub-route is registered only at /v1beta1, and the v1beta1 GetDriveItem handler is share-jail-only, so the v1beta1 mount of the middleware is exercised through the permissions scenario, since there is no other v1beta1 endpoint that works for regular personal-drive items. --- tests/acceptance/bootstrap/GraphContext.php | 165 ++++++++++++++++++ .../apiGraph/colonSyntaxPathLookup.feature | 114 ++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 tests/acceptance/features/apiGraph/colonSyntaxPathLookup.feature diff --git a/tests/acceptance/bootstrap/GraphContext.php b/tests/acceptance/bootstrap/GraphContext.php index cf350be6be..70254354c0 100644 --- a/tests/acceptance/bootstrap/GraphContext.php +++ b/tests/acceptance/bootstrap/GraphContext.php @@ -3334,4 +3334,169 @@ class GraphContext implements Context { $response = $this->unmarkFavorite($user, $itemId); $this->featureContext->theHTTPStatusCodeShouldBe(204, '', $response); } + + /** + * Encode a colon-syntax path segment so slashes survive but the + * structural ":" delimiters of the Graph URL remain literal. + * + * @param string $path + * + * @return string + */ + private function encodeColonPathSegment(string $path): string { + $path = \ltrim($path, '/'); + $parts = \explode('/', $path); + $encoded = \array_map('rawurlencode', $parts); + return \implode('/', $encoded); + } + + /** + * Send a Graph API request and capture the response on FeatureContext. + * + * @param string $user + * @param string $method + * @param string $relativeUrl + * + * @return void + */ + private function sendGraphRequestAndCaptureResponse( + string $user, + string $method, + string $relativeUrl + ): void { + $response = $this->featureContext->sendingToWithDirectUrl($user, $method, $relativeUrl); + $this->featureContext->setResponse($response); + } + + /** + * @When /^user "([^"]*)" gets the drive item with colon path "([^"]*)" of space "([^"]*)" using the Graph API version "(v1\.0|v1beta1)"$/ + * + * Hits /graph/{version}/drives/{driveID}/root:/{path}, exercising the + * colon-syntax path lookup middleware (root-anchored, no suffix). + * + * @param string $user + * @param string $path + * @param string $spaceName + * @param string $apiVersion + * + * @return void + */ + public function userGetsDriveItemWithColonPathOfSpace( + string $user, + string $path, + string $spaceName, + string $apiVersion + ): void { + $driveId = $this->spacesContext->getSpaceIdByName($user, $spaceName); + $encoded = $this->encodeColonPathSegment($path); + $url = "/graph/$apiVersion/drives/$driveId/root:/$encoded"; + $this->sendGraphRequestAndCaptureResponse($user, "GET", $url); + } + + /** + * @When /^user "([^"]*)" gets the drive item with colon path "([^"]*)" of space "([^"]*)" with trailing colon using the Graph API version "(v1\.0|v1beta1)"$/ + * + * Hits /graph/{version}/drives/{driveID}/root:/{path}: with the optional + * trailing ":" — verifies the middleware accepts both forms. + * + * @param string $user + * @param string $path + * @param string $spaceName + * @param string $apiVersion + * + * @return void + */ + public function userGetsDriveItemWithColonPathOfSpaceWithTrailingColon( + string $user, + string $path, + string $spaceName, + string $apiVersion + ): void { + $driveId = $this->spacesContext->getSpaceIdByName($user, $spaceName); + $encoded = $this->encodeColonPathSegment($path); + $url = "/graph/$apiVersion/drives/$driveId/root:/$encoded:"; + $this->sendGraphRequestAndCaptureResponse($user, "GET", $url); + } + + /** + * @When /^user "([^"]*)" gets the drive item with colon path "([^"]*)" relative to folder "([^"]*)" of space "([^"]*)" using the Graph API version "(v1\.0|v1beta1)"$/ + * + * Hits /graph/{version}/drives/{driveID}/items/{itemID}:/{relPath}, the + * item-anchored colon-syntax form. + * + * @param string $user + * @param string $relPath + * @param string $folderName + * @param string $spaceName + * @param string $apiVersion + * + * @return void + */ + public function userGetsDriveItemWithColonPathRelativeToFolderOfSpace( + string $user, + string $relPath, + string $folderName, + string $spaceName, + string $apiVersion + ): void { + $driveId = $this->spacesContext->getSpaceIdByName($user, $spaceName); + $folderId = $this->spacesContext->getResourceId($user, $spaceName, $folderName); + $encoded = $this->encodeColonPathSegment($relPath); + $url = "/graph/$apiVersion/drives/$driveId/items/$folderId:/$encoded"; + $this->sendGraphRequestAndCaptureResponse($user, "GET", $url); + } + + /** + * @When /^user "([^"]*)" lists permissions of the drive item with colon path "([^"]*)" of space "([^"]*)" using the Graph API version "(v1\.0|v1beta1)"$/ + * + * Hits /graph/{version}/drives/{driveID}/root:/{path}:/permissions, the + * "colon path with suffix" form (root-anchored colon path + canonical + * sub-route). + * + * @param string $user + * @param string $path + * @param string $spaceName + * @param string $apiVersion + * + * @return void + */ + public function userListsPermissionsOfDriveItemWithColonPathOfSpace( + string $user, + string $path, + string $spaceName, + string $apiVersion + ): void { + $driveId = $this->spacesContext->getSpaceIdByName($user, $spaceName); + $encoded = $this->encodeColonPathSegment($path); + $url = "/graph/$apiVersion/drives/$driveId/root:/$encoded:/permissions"; + $this->sendGraphRequestAndCaptureResponse($user, "GET", $url); + } + + /** + * @When /^user "([^"]*)" gets the drive item with colon path "([^"]*)" of the personal space of "([^"]*)" using the Graph API version "(v1\.0|v1beta1)"$/ + * + * Same as userGetsDriveItemWithColonPathOfSpace, but the request is + * issued by :user against the personal space drive ID of :owner. Used + * for security tests where one user attempts to reach another user's + * resource via colon syntax — should be indistinguishable from a + * not-found result. + * + * @param string $user + * @param string $path + * @param string $owner + * @param string $apiVersion + * + * @return void + */ + public function userGetsDriveItemWithColonPathOfPersonalSpaceOf( + string $user, + string $path, + string $owner, + string $apiVersion + ): void { + $driveId = $this->spacesContext->getSpaceIdByName($owner, "Personal"); + $encoded = $this->encodeColonPathSegment($path); + $url = "/graph/$apiVersion/drives/$driveId/root:/$encoded"; + $this->sendGraphRequestAndCaptureResponse($user, "GET", $url); + } } diff --git a/tests/acceptance/features/apiGraph/colonSyntaxPathLookup.feature b/tests/acceptance/features/apiGraph/colonSyntaxPathLookup.feature new file mode 100644 index 0000000000..d8e445f769 --- /dev/null +++ b/tests/acceptance/features/apiGraph/colonSyntaxPathLookup.feature @@ -0,0 +1,114 @@ +Feature: colon-syntax path lookup on the Graph API + As a client + I want to address drive items by path using colon-syntax URLs on the Graph API + So that I do not have to walk the path with successive lookups before issuing a request + + The colon-syntax shapes recognised by the path-lookup middleware are: + + /graph/{version}/drives/{driveID}/root:/[:/][:] + /graph/{version}/drives/{driveID}/items/{itemID}:/[:/][:] + + Both /v1.0 and /v1beta1 are supported. NOT_FOUND and PERMISSION_DENIED + collapse to 404 so the middleware never discloses the existence of + resources the caller is not allowed to see. + + Background: + Given user "Alice" has been created with default attributes + And user "Alice" has created folder "folder1" + And user "Alice" has created folder "folder1/sub" + And user "Alice" has uploaded file with content "hello" to "folder1/file.txt" + And user "Alice" has uploaded file with content "deep" to "folder1/sub/deep.txt" + + + Scenario: get a drive item by root-anchored colon path + When user "Alice" gets the drive item with colon path "folder1/file.txt" of space "Personal" using the Graph API version "v1.0" + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["id", "name", "parentReference"], + "properties": { + "id": { + "type": "string", + "pattern": "^%file_id_pattern%$" + }, + "name": { + "const": "file.txt" + } + } + } + """ + + + Scenario: get a drive item by root-anchored colon path with a deep path + When user "Alice" gets the drive item with colon path "folder1/sub/deep.txt" of space "Personal" using the Graph API version "v1.0" + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["id", "name"], + "properties": { + "name": { + "const": "deep.txt" + } + } + } + """ + + + Scenario: get a drive item by root-anchored colon path with a trailing colon + When user "Alice" gets the drive item with colon path "folder1/file.txt" of space "Personal" with trailing colon using the Graph API version "v1.0" + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["id", "name"], + "properties": { + "name": { + "const": "file.txt" + } + } + } + """ + + + Scenario: get a drive item by item-anchored colon path + When user "Alice" gets the drive item with colon path "file.txt" relative to folder "folder1" of space "Personal" using the Graph API version "v1.0" + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["id", "name"], + "properties": { + "name": { + "const": "file.txt" + } + } + } + """ + + + Scenario: list permissions of a drive item via colon path with a sub-route suffix on v1beta1 + # Exercises the "/:/" rewrite shape and, at the same time, + # the v1beta1 mount of the middleware. The /permissions sub-route is only + # registered at /v1beta1/, and the canonical /v1beta1 GetDriveItem + # handler is share-jail-only, so this is the cleanest way to assert that + # the v1beta1 colon-syntax path actually reaches a working handler for + # regular drive items. + When user "Alice" lists permissions of the drive item with colon path "folder1" of space "Personal" using the Graph API version "v1beta1" + Then the HTTP status code should be "200" + + + Scenario: non-existent colon path returns 404 + When user "Alice" gets the drive item with colon path "folder1/does-not-exist.txt" of space "Personal" using the Graph API version "v1.0" + Then the HTTP status code should be "404" + + + Scenario: another user cannot disclose existence of a resource via colon path + Given user "Brian" has been created with default attributes + When user "Brian" gets the drive item with colon path "folder1/file.txt" of the personal space of "Alice" using the Graph API version "v1.0" + Then the HTTP status code should be "404"