From b3296f626788a3f0ed69b362e9638a220d93cc8d Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 09:43:27 -0400 Subject: [PATCH 01/27] test(WebDav): add reusable DAV client helpers + standardize error handling Eliminates a bunch of duplicate boilerplate Signed-off-by: Josh --- .../integration/features/bootstrap/WebDav.php | 275 +++++++----------- 1 file changed, 100 insertions(+), 175 deletions(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index fb552ce785b75..6acc86880eac1 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -69,25 +69,82 @@ public function getDavFilesPath($user) { } } - public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = 'files') { + /** + * Returns the base URL for DAV operations (strips trailing "/ocs" from baseUrl). + */ + private function getDavBaseUrl(): string { + return substr($this->baseUrl, 0, -4); + } + + /** + * Resolves the full DAV endpoint URL for a given user, path, and request type. + * + * @param string|null $user The acting user + * @param string $path The resource path (e.g. "/myFile.txt") + * @param string $type One of 'files', 'uploads', or a DAV sub-collection name + * @return string The fully qualified URL + */ + private function getDavUrl(?string $user, string $path, string $type = 'files'): string { + $base = $this->getDavBaseUrl(); + if ($type === 'files') { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . "$path"; + return $base . $this->getDavFilesPath($user) . $path; } elseif ($type === 'uploads') { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . "$path"; - } else { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path"; + return $base . $this->davPath . $path; } - $client = new GClient(); + + return $base . $this->davPath . '/' . $type . $path; + } + + /** + * Returns the auth credentials for a user as a [username, password] tuple + * suitable for both Guzzle's 'auth' option and Sabre client construction. + * + * @param string|null $user + * @return array{string, string}|null Null if user is empty (unauthenticated) + */ + private function getAuthForUser(?string $user): ?array { + if ($user === 'admin') { + return $this->adminUser; + } elseif ($user !== null && $user !== '') { + return [$user, $this->regularUser]; + } + + return null; + } + + /** + * Returns a pre-configured Sabre DAV client for the given user. + */ + public function getSabreClient(string $user): SClient { + $auth = $this->getAuthForUser($user); + + return new SClient([ + 'baseUri' => $this->getDavBaseUrl(), + 'userName' => $auth[0] ?? $user, + 'password' => $auth[1] ?? '', + 'authType' => SClient::AUTH_BASIC, + ]); + } + + /** + * Returns the full DAV URL prefix for Destination headers, etc. + */ + public function getFullDavFilesUrl(string $user): string { + return $this->getDavBaseUrl() . $this->getDavFilesPath($user); + } + + public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = 'files') { $options = [ 'headers' => $headers, - 'body' => $body + 'body' => $body, + 'http_errors' => false, ]; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } elseif ($user !== '') { - $options['auth'] = [$user, $this->regularUser]; + $auth = $this->getAuthForUser($user); + if ($user !== null) { + $options['auth'] = $auth; } - return $client->request($method, $fullUrl, $options); + return (new GClient())->request($method, $this->getDavUrl($user, $path, $type), $options); } /** @@ -110,13 +167,8 @@ public function userMovedFile($user, $entry, $fileSource, $fileDestination) { * @param string $fileDestination */ public function userMovesFile($user, $entry, $fileSource, $fileDestination) { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); - $headers['Destination'] = $fullUrl . $fileDestination; - try { - $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $headers['Destination'] = $this->getFullDavFilesUrl($user) . $fileDestination; + $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); } /** @@ -126,14 +178,8 @@ public function userMovesFile($user, $entry, $fileSource, $fileDestination) { * @param string $fileDestination */ public function userCopiesFileTo($user, $fileSource, $fileDestination) { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); - $headers['Destination'] = $fullUrl . $fileDestination; - try { - $this->response = $this->makeDavRequest($user, 'COPY', $fileSource, $headers); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx and 5xx responses cause an exception - $this->response = $e->getResponse(); - } + $headers['Destination'] = $this->getFullDavFilesUrl($user) . $fileDestination; + $this->response = $this->makeDavRequest($user, 'COPY', $fileSource, $headers); } /** @@ -243,11 +289,7 @@ public function downloadedContentWhenDownloadindShouldBe($fileSource, $range, $c * @When Downloading folder :folderName */ public function downloadingFolder(string $folderName) { - try { - $this->response = $this->makeDavRequest($this->currentUser, 'GET', $folderName, ['Accept' => 'application/zip']); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($this->currentUser, 'GET', $folderName, ['Accept' => 'application/zip']); } /** @@ -275,11 +317,7 @@ public function downloadPublicFolder(string $folderName) { * @param string $fileName */ public function downloadingFile($fileName) { - try { - $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileName, []); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileName, []); } /** @@ -588,19 +626,8 @@ public function searchFile(string $user, ?string $properties = null, ?string $sc '; - try { - $this->response = $this->makeDavRequest($user, 'SEARCH', '', [ - 'Content-Type' => 'text/xml' - ], $body, ''); - - var_dump((string)$this->response->getBody()); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($user, 'SEARCH', '', ['Content-Type' => 'text/xml'], $body, ''); + var_dump((string)$this->response->getBody()); } /* Returns the elements of a report command @@ -635,24 +662,6 @@ public function makeSabrePath($user, $path, $type = 'files') { } } - public function getSabreClient($user) { - $fullUrl = substr($this->baseUrl, 0, -4); - - $settings = [ - 'baseUri' => $fullUrl, - 'userName' => $user, - ]; - - if ($user === 'admin') { - $settings['password'] = $this->adminUser[1]; - } else { - $settings['password'] = $this->regularUser; - } - $settings['authType'] = SClient::AUTH_BASIC; - - return new SClient($settings); - } - /** * @Then /^user "([^"]*)" should see following elements$/ * @param string $user @@ -680,15 +689,7 @@ public function checkElementList($user, $expectedElements) { */ public function userUploadsAFileTo($user, $source, $destination) { $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); - try { - $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); } /** @@ -712,15 +713,7 @@ public function userAddsAFileTo($user, $bytes, $destination) { */ public function userUploadsAFileWithContentTo($user, $content, $destination) { $file = \GuzzleHttp\Psr7\Utils::streamFor($content); - try { - $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); } /** @@ -730,15 +723,7 @@ public function userUploadsAFileWithContentTo($user, $content, $destination) { * @param string $file */ public function userDeletesFile($user, $type, $file) { - try { - $this->response = $this->makeDavRequest($user, 'DELETE', $file, []); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($user, 'DELETE', $file, []); } /** @@ -747,16 +732,8 @@ public function userDeletesFile($user, $type, $file) { * @param string $destination */ public function userCreatedAFolder($user, $destination) { - try { - $destination = '/' . ltrim($destination, '/'); - $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, []); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $destination = '/' . ltrim($destination, '/'); + $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, []); } /** @@ -836,10 +813,8 @@ public function userUploadsNewChunkFileOfWithToId($user, $num, $data, $id) { */ public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest) { $source = '/uploads/' . $user . '/' . $id . '/.file'; - $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest; - $this->makeDavRequest($user, 'MOVE', $source, [ - 'Destination' => $destination - ], null, 'uploads'); + $headers['Destination'] = $this->getFullDavFilesUrl($user) . $dest; + $this->makeDavRequest($user, 'MOVE', $source, $headers, null, 'uploads'); } /** @@ -847,16 +822,9 @@ public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest) { */ public function userMovesNewChunkFileWithIdToMychunkedfileWithSize($user, $id, $dest, $size) { $source = '/uploads/' . $user . '/' . $id . '/.file'; - $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest; - - try { - $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ - 'Destination' => $destination, - 'OC-Total-Length' => $size - ], null, 'uploads'); - } catch (\GuzzleHttp\Exception\BadResponseException $ex) { - $this->response = $ex->getResponse(); - } + $headers['Destination'] = $this->getFullDavFilesUrl($user) . $dest; + $headers['OC-Total-Length' => $size]; + $this->response = $this->makeDavRequest($user, 'MOVE', $source, $headers, null, 'uploads'); } @@ -867,9 +835,8 @@ public function userCreatesANewChunkingv2UploadWithIdAndDestination($user, $id, $this->s3MultipartDestination = $this->getTargetDestination($user, $targetDestination); $this->newUploadId(); $destination = '/uploads/' . $user . '/' . $this->getUploadId($id); - $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [ - 'Destination' => $this->s3MultipartDestination, - ], null, 'uploads'); + $headers['Destination'] = $this->s3MultipartDestination; + $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, $headers, null, 'uploads'); } /** @@ -878,9 +845,8 @@ public function userCreatesANewChunkingv2UploadWithIdAndDestination($user, $id, public function userUploadsNewChunkv2FileToIdAndDestination($user, $num, $id) { $data = \GuzzleHttp\Psr7\Utils::streamFor(fopen('/tmp/part-upload-' . $num, 'r')); $destination = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/' . $num; - $this->response = $this->makeDavRequest($user, 'PUT', $destination, [ - 'Destination' => $this->s3MultipartDestination - ], $data, 'uploads'); + $headers['Destination'] = $this->s3MultipartDestination; + $this->response = $this->makeDavRequest($user, 'PUT', $destination, $headers, $data, 'uploads'); } /** @@ -888,21 +854,12 @@ public function userUploadsNewChunkv2FileToIdAndDestination($user, $num, $id) { */ public function userMovesNewChunkv2FileWithIdToMychunkedfileAndDestination($user, $id) { $source = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/.file'; - try { - $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ - 'Destination' => $this->s3MultipartDestination, - ], null, 'uploads'); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $headers['Destination'] = $this->s3MultipartDestination; + $this->response = $this->makeDavRequest($user, 'MOVE', $source, $headers, null, 'uploads'); } private function getTargetDestination(string $user, string $destination): string { - return substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $destination; + return $this->getFullDavFilesUrl($user) . $destination; } private function getUploadId(string $id): string { @@ -917,15 +874,7 @@ private function newUploadId() { * @Given /^Downloading file "([^"]*)" as "([^"]*)"$/ */ public function downloadingFileAs($fileName, $user) { - try { - $this->response = $this->makeDavRequest($user, 'GET', $fileName, []); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($user, 'GET', $fileName, []); } /** @@ -955,27 +904,11 @@ public function userUnfavoritesElement($user, $path) { /*Set the elements of a proppatch, $folderDepth requires 1 to see elements without children*/ public function changeFavStateOfAnElement($user, $path, $favOrUnfav, $folderDepth, $properties = null) { - $fullUrl = substr($this->baseUrl, 0, -4); - $settings = [ - 'baseUri' => $fullUrl, - 'userName' => $user, - ]; - if ($user === 'admin') { - $settings['password'] = $this->adminUser[1]; - } else { - $settings['password'] = $this->regularUser; - } - $settings['authType'] = SClient::AUTH_BASIC; - - $client = new SClient($settings); + $client = $this->getSabreClient($user); if (!$properties) { - $properties = [ - '{http://owncloud.org/ns}favorite' => $favOrUnfav - ]; + $properties = ['{http://owncloud.org/ns}favorite' => $favOrUnfav]; } - - $response = $client->proppatch($this->getDavFilesPath($user) . $path, $properties, $folderDepth); - return $response; + return $client->proppatch($this->getDavFilesPath($user) . $path, $properties, $folderDepth); } /** @@ -1010,11 +943,7 @@ public function checkIfETAGHasChanged($path, $user) { * @When Connecting to dav endpoint */ public function connectingToDavEndpoint() { - try { - $this->response = $this->makeDavRequest(null, 'PROPFIND', '', []); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest(null, 'PROPFIND', '', []); } /** @@ -1027,11 +956,7 @@ public function requestingShareNote() { } else { $token = $this->lastShareData->data->token; } - try { - $this->response = $this->makeDavRequest('', 'PROPFIND', $token, [], $propfind); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest('', 'PROPFIND', $token, [], $propfind); } /** From 4824ecc9f818d9b2b8d306aad910fb2e1b2faff1 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 10:17:04 -0400 Subject: [PATCH 02/27] test(WebDAV): create central Guzzle client helper Eliminates duplicate boilerplate code Signed-off-by: Josh --- .../integration/features/bootstrap/WebDav.php | 105 +++++++----------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 6acc86880eac1..1f1fdff13aa17 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -127,6 +127,28 @@ public function getSabreClient(string $user): SClient { ]); } + /** + * Returns a pre-configured Guzzle client for the given user. + * + * Centralizes: base URL, auth resolution, http_errors suppression, + * and any future cross-cutting middleware (logging, retries, etc.). + * + * @param string|null $user The acting user, 'admin', or null/empty for unauthenticated + * @param array $extraConfig Additional Guzzle constructor config to merge + */ + public function getGuzzleClient(?string $user = null, array $extraConfig = []): GClient { + $config = [ + 'http_errors' => false, + ]; + + $auth = $this->getAuthForUser($user); + if ($auth !== null) { + $config['auth'] = $auth; + } + + return new GClient(array_merge($config, $extraConfig)); + } + /** * Returns the full DAV URL prefix for Destination headers, etc. */ @@ -135,16 +157,12 @@ public function getFullDavFilesUrl(string $user): string { } public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = 'files') { + $client = $this->getGuzzleClient($user); $options = [ 'headers' => $headers, 'body' => $body, - 'http_errors' => false, ]; - $auth = $this->getAuthForUser($user); - if ($user !== null) { - $options['auth'] = $auth; - } - return (new GClient())->request($method, $this->getDavUrl($user, $path, $type), $options); + return $client->request($method, $this->getDavUrl($user, $path, $type), $options); } /** @@ -198,14 +216,9 @@ public function downloadFileWithRange($fileSource, $range) { */ public function downloadPublicFileWithRange($range) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token"; - - $client = new GClient(); - $options = []; - $options['headers'] = [ - 'Range' => $range - ]; - + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token"; + $client = $this->getGuzzleClient(null); + $options['headers'] = [ 'Range' => $range ]; $this->response = $client->request('GET', $fullUrl, $options); } @@ -215,15 +228,9 @@ public function downloadPublicFileWithRange($range) { */ public function downloadPublicFileInsideAFolderWithRange($path, $range) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$path"; - - $client = new GClient(); - $options = [ - 'headers' => [ - 'Range' => $range - ] - ]; - + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$path"; + $client = $this->getGuzzleClient(null); + $options['headers'] = [ 'Range' => $range ]; $this->response = $client->request('GET', $fullUrl, $options); } @@ -297,19 +304,10 @@ public function downloadingFolder(string $folderName) { */ public function downloadPublicFolder(string $folderName) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$folderName"; - - $client = new GClient(); - $options = []; - $options['headers'] = [ - 'Accept' => 'application/zip' - ]; - - try { - $this->response = $client->request('GET', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$folderName"; + $client = $this->getGuzzleClient(null); + $options['headers'] = [ 'Accept' => 'application/zip' ]; + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -325,20 +323,10 @@ public function downloadingFile($fileName) { */ public function downloadingPublicFile(string $filename) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$filename"; - - $client = new GClient(); - $options = [ - 'headers' => [ - 'X-Requested-With' => 'XMLHttpRequest', - ] - ]; - - try { - $this->response = $client->request('GET', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$filename"; + $client = $this->getGuzzleClient(null); + $options['headers' = [ 'X-Requested-With' => 'XMLHttpRequest' ]; + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -346,14 +334,9 @@ public function downloadingPublicFile(string $filename) { */ public function downloadingPublicFileWithoutHeader(string $filename) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$filename"; - - $client = new GClient(); - try { - $this->response = $client->request('GET', $fullUrl); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$filename"; + $client = $this->getGuzzleClient(null); + $this->response = $client->request('GET', $fullUrl); } /** @@ -777,17 +760,15 @@ public function userUploadsBulkedFiles($user, $name1, $content1, $name2, $conten fwrite($stream, $body); rewind($stream); - $client = new GClient(); + $client = $this->getGuzzleClient($user); $options = [ - 'auth' => [$user, $this->regularUser], 'headers' => [ 'Content-Type' => 'multipart/related; boundary=' . $boundary, 'Content-Length' => (string)strlen($body), ], 'body' => $body ]; - - return $client->request('POST', substr($this->baseUrl, 0, -4) . 'remote.php/dav/bulk', $options); + return $client->request('POST', $this->getDavBaseUrl() . 'remote.php/dav/bulk', $options); } /** From ba8d6a020c306038a1667dfd31d8daa85bd11005 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 10:29:30 -0400 Subject: [PATCH 03/27] test(WebDav): fixup typo Signed-off-by: Josh --- build/integration/features/bootstrap/WebDav.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 1f1fdff13aa17..7ec15875204a9 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -325,7 +325,7 @@ public function downloadingPublicFile(string $filename) { $token = $this->lastShareData->data->token; $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$filename"; $client = $this->getGuzzleClient(null); - $options['headers' = [ 'X-Requested-With' => 'XMLHttpRequest' ]; + $options['headers'] = [ 'X-Requested-With' => 'XMLHttpRequest' ]; $this->response = $client->request('GET', $fullUrl, $options); } From 3b2a383a1c0b99ac3fb4f44f5f132f69d7fe6574 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 10:32:11 -0400 Subject: [PATCH 04/27] test(WebDav): fixup another typo Signed-off-by: Josh --- build/integration/features/bootstrap/WebDav.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 7ec15875204a9..0fb8b2a89d834 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -804,7 +804,7 @@ public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest) { public function userMovesNewChunkFileWithIdToMychunkedfileWithSize($user, $id, $dest, $size) { $source = '/uploads/' . $user . '/' . $id . '/.file'; $headers['Destination'] = $this->getFullDavFilesUrl($user) . $dest; - $headers['OC-Total-Length' => $size]; + $headers['OC-Total-Length'] = $size; $this->response = $this->makeDavRequest($user, 'MOVE', $source, $headers, null, 'uploads'); } From 5bafd37a55707aa4a03f51cf2b2a59bf7a013c92 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 12:13:16 -0400 Subject: [PATCH 05/27] test(integration): refactor FilesDropContext to use new DAV helpers Signed-off-by: Josh --- .../features/bootstrap/FilesDropContext.php | 55 ++++--------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 4ccb58f08646f..148bb5224f637 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -17,32 +17,15 @@ class FilesDropContext implements Context, SnippetAcceptingContext { * @When Dropping file :path with :content */ public function droppingFileWith($path, $content, $nickname = null) { - $client = new Client(); - $options = []; - if (count($this->lastShareData->data->element) > 0) { - $token = $this->lastShareData->data[0]->token; - } else { - $token = $this->lastShareData->data[0]->token; - } - - $base = substr($this->baseUrl, 0, -4); - $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$path"); - - $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest', - ]; - + $token = $this->lastShareData->data[0]->token; + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$path"; + $client = $this->getGuzzleClient(null); + $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; + $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); if ($nickname) { $options['headers']['X-NC-NICKNAME'] = $nickname; } - - $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); - - try { - $this->response = $client->request('PUT', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $client->request('PUT', $fullUrl, $options); } @@ -58,30 +41,14 @@ public function droppingFileWithAs($path, $content, $nickname) { * @When Creating folder :folder in drop */ public function creatingFolderInDrop($folder, $nickname = null) { - $client = new Client(); - $options = []; - if (count($this->lastShareData->data->element) > 0) { - $token = $this->lastShareData->data[0]->token; - } else { - $token = $this->lastShareData->data[0]->token; - } - - $base = substr($this->baseUrl, 0, -4); - $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$folder"); - - $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest', - ]; - + $token = $this->lastShareData->data[0]->token; + $fullUrl = s$this->getDavBaseUrl() . "public.php/dav/files/$token/$folder"); + $client = $this->getGuzzleClient(null); + $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; if ($nickname) { $options['headers']['X-NC-NICKNAME'] = $nickname; } - - try { - $this->response = $client->request('MKCOL', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $client->request('MKCOL', $fullUrl, $options); } From 461deb9d7ee36f81f8ce26fff401bcea9fb9afe7 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 12:15:56 -0400 Subject: [PATCH 06/27] test(FilesDropContext): fix typo Signed-off-by: Josh --- build/integration/features/bootstrap/FilesDropContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 148bb5224f637..884396bd536a1 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -42,7 +42,7 @@ public function droppingFileWithAs($path, $content, $nickname) { */ public function creatingFolderInDrop($folder, $nickname = null) { $token = $this->lastShareData->data[0]->token; - $fullUrl = s$this->getDavBaseUrl() . "public.php/dav/files/$token/$folder"); + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$folder"); $client = $this->getGuzzleClient(null); $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; if ($nickname) { From a61cefcf3cfa7bf647aa9302f591b3b47f1bd1cc Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 12:20:31 -0400 Subject: [PATCH 07/27] test(FilesDropContext): fix typo Signed-off-by: Josh --- build/integration/features/bootstrap/FilesDropContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 884396bd536a1..8eb0104b47329 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -42,7 +42,7 @@ public function droppingFileWithAs($path, $content, $nickname) { */ public function creatingFolderInDrop($folder, $nickname = null) { $token = $this->lastShareData->data[0]->token; - $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$folder"); + $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$folder"; $client = $this->getGuzzleClient(null); $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; if ($nickname) { From abdbd39424566a77ca15be7434be95bb63a2a479 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 12:24:16 -0400 Subject: [PATCH 08/27] test(WebDav): make cs happy Signed-off-by: Josh --- .../integration/features/bootstrap/WebDav.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 0fb8b2a89d834..fbed1d04ee796 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -79,10 +79,10 @@ private function getDavBaseUrl(): string { /** * Resolves the full DAV endpoint URL for a given user, path, and request type. * - * @param string|null $user The acting user - * @param string $path The resource path (e.g. "/myFile.txt") - * @param string $type One of 'files', 'uploads', or a DAV sub-collection name - * @return string The fully qualified URL + * @param string|null $user The acting user + * @param string $path The resource path (e.g. "/myFile.txt") + * @param string $type One of 'files', 'uploads', or a DAV sub-collection name + * @return string The fully qualified URL */ private function getDavUrl(?string $user, string $path, string $type = 'files'): string { $base = $this->getDavBaseUrl(); @@ -101,7 +101,7 @@ private function getDavUrl(?string $user, string $path, string $type = 'files'): * suitable for both Guzzle's 'auth' option and Sabre client construction. * * @param string|null $user - * @return array{string, string}|null Null if user is empty (unauthenticated) + * @return array{string, string}|null Null if user is empty (unauthenticated) */ private function getAuthForUser(?string $user): ?array { if ($user === 'admin') { @@ -120,7 +120,7 @@ public function getSabreClient(string $user): SClient { $auth = $this->getAuthForUser($user); return new SClient([ - 'baseUri' => $this->getDavBaseUrl(), + 'baseUri' => $this->getDavBaseUrl(), 'userName' => $auth[0] ?? $user, 'password' => $auth[1] ?? '', 'authType' => SClient::AUTH_BASIC, @@ -133,8 +133,8 @@ public function getSabreClient(string $user): SClient { * Centralizes: base URL, auth resolution, http_errors suppression, * and any future cross-cutting middleware (logging, retries, etc.). * - * @param string|null $user The acting user, 'admin', or null/empty for unauthenticated - * @param array $extraConfig Additional Guzzle constructor config to merge + * @param string|null $user The acting user, 'admin', or null/empty for unauthenticated + * @param array $extraConfig Additional Guzzle constructor config to merge */ public function getGuzzleClient(?string $user = null, array $extraConfig = []): GClient { $config = [ @@ -144,7 +144,7 @@ public function getGuzzleClient(?string $user = null, array $extraConfig = []): $auth = $this->getAuthForUser($user); if ($auth !== null) { $config['auth'] = $auth; - } + } return new GClient(array_merge($config, $extraConfig)); } @@ -155,7 +155,7 @@ public function getGuzzleClient(?string $user = null, array $extraConfig = []): public function getFullDavFilesUrl(string $user): string { return $this->getDavBaseUrl() . $this->getDavFilesPath($user); } - + public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = 'files') { $client = $this->getGuzzleClient($user); $options = [ @@ -804,7 +804,7 @@ public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest) { public function userMovesNewChunkFileWithIdToMychunkedfileWithSize($user, $id, $dest, $size) { $source = '/uploads/' . $user . '/' . $id . '/.file'; $headers['Destination'] = $this->getFullDavFilesUrl($user) . $dest; - $headers['OC-Total-Length'] = $size; + $headers['OC-Total-Length'] = $size; $this->response = $this->makeDavRequest($user, 'MOVE', $source, $headers, null, 'uploads'); } From 19c87864fe770b19473f2d040f4731542ee66c0b Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 20:54:56 -0400 Subject: [PATCH 09/27] test(FilesDropContext): fixup for cs Signed-off-by: Josh --- build/integration/features/bootstrap/FilesDropContext.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 8eb0104b47329..54fa8eedbcd21 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -6,7 +6,6 @@ */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; -use GuzzleHttp\Client; require __DIR__ . '/autoload.php'; @@ -25,7 +24,7 @@ public function droppingFileWith($path, $content, $nickname = null) { if ($nickname) { $options['headers']['X-NC-NICKNAME'] = $nickname; } - $this->response = $client->request('PUT', $fullUrl, $options); + $this->response = $client->request('PUT', $fullUrl, $options); } @@ -43,7 +42,7 @@ public function droppingFileWithAs($path, $content, $nickname) { public function creatingFolderInDrop($folder, $nickname = null) { $token = $this->lastShareData->data[0]->token; $fullUrl = $this->getDavBaseUrl() . "public.php/dav/files/$token/$folder"; - $client = $this->getGuzzleClient(null); + $client = $this->getGuzzleClient(null); $options['headers'] = [ 'X-REQUESTED-WITH' => 'XMLHttpRequest' ]; if ($nickname) { $options['headers']['X-NC-NICKNAME'] = $nickname; From a97c8d28febeef75feb67740162464acab87784c Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 22:05:29 -0400 Subject: [PATCH 10/27] test(provisioning-v1.feature): fix broken (typod URL) subadmin scenario Signed-off-by: Josh --- build/integration/features/provisioning-v1.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 62b58279c616c..c9037b39f4c40 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -858,7 +858,7 @@ Feature: provisioning And Assure user "subadmin" is subadmin of group "new-group" And assure user "subadmin" is disabled And As an "subadmin" - When sending "PUT" to "/cloud/users/subadmin/enabled" + When sending "PUT" to "/cloud/users/subadmin/enable" And As an "admin" And user "subadmin" is disabled From 670352f361c42dc616f212d086e21361b3d895ca Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 22:55:02 -0400 Subject: [PATCH 11/27] test(integration): refactor BasicStructure to use new Guzzle helpers Signed-off-by: Josh --- .../features/bootstrap/BasicStructure.php | 187 ++++-------------- 1 file changed, 41 insertions(+), 146 deletions(-) diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index 8d1d3b38793f5..bdf4120d9c8b2 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -6,10 +6,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Gherkin\Node\TableNode; -use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; @@ -22,26 +19,13 @@ trait BasicStructure { use Mail; use Theming; - /** @var string */ - private $currentUser = ''; - - /** @var string */ - private $currentServer = ''; - - /** @var string */ - private $baseUrl = ''; - - /** @var int */ - private $apiVersion = 1; - - /** @var ResponseInterface */ - private $response = null; - - /** @var CookieJar */ - private $cookieJar; - - /** @var string */ - private $requestToken; + private string $currentUser = ''; + private string $currentServer = ''; + private string $baseUrl = ''; + private int $apiVersion = 1; + private ResponseInterface $response = null; + private CookieJar $cookieJar; + private string $requestToken; protected $adminUser; protected $regularUser; @@ -146,63 +130,24 @@ public function simplifyArray(array $arrayOfArrays): array { */ public function sendingToWith(string $verb, string $url, ?TableNode $body): void { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } elseif (strpos($this->currentUser, 'anonymous') !== 0) { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIRequest' => 'true' - ]; + $client = $this->getGuzzleClient($this->currentUser); + $options['headers'] = [ 'OCS-APIRequest' => 'true' ]; if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - } - - // TODO: Fix this hack! - if ($verb === 'PUT' && $body === null) { - $options['form_params'] = [ - 'foo' => 'bar', - ]; - } - - try { - $this->response = $client->request($verb, $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } catch (ServerException $ex) { - $this->response = $ex->getResponse(); + $options['form_params'] = $body->getRowsHash(); } + $this->response = $client->request($verb, $fullUrl, $options); } protected function sendRequestForJSON(string $verb, string $url, TableNode|array|null $body = null, array $headers = []): void { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = ['admin', 'admin']; - } elseif (strpos($this->currentUser, 'anonymous') !== 0) { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } + $client = $this->getGuzzleClient($this->currentUser); if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; + $options['form_params'] = $body->getRowsHash(); } elseif (is_array($body)) { $options['form_params'] = $body; } - - $options['headers'] = array_merge($headers, [ - 'OCS-ApiRequest' => 'true', - 'Accept' => 'application/json', - ]); - - try { - $this->response = $client->{$verb}($fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } + $options['headers'] = array_merge($headers, [ 'OCS-APIRequest' => 'true', 'Accept' => 'application/json' ]); + $this->response = $client->{$verb}($fullUrl, $options); } /** @@ -215,24 +160,12 @@ public function sendingToDirectUrl($verb, $url) { } public function sendingToWithDirectUrl($verb, $url, $body) { - $fullUrl = substr($this->baseUrl, 0, -5) . $url; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } elseif (strpos($this->currentUser, 'anonymous') !== 0) { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } + $fullUrl = substr($this->baseUrl, 0, -5) . $url; // drops `/ocs/` + $client = $this->getGuzzleClient($this->currentUser); if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - } - - try { - $this->response = $client->request($verb, $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); + $options['form_params'] = $body->getRowsHash(); } + $this->response = $client->request($verb, $fullUrl, $options); } public function isExpectedUrl($possibleUrl, $finalPart) { @@ -268,7 +201,7 @@ public function theContentTypeShouldbe($contentType) { /** * @param ResponseInterface $response */ - private function extracRequestTokenFromResponse(ResponseInterface $response) { + private function extractRequestTokenFromResponse(ResponseInterface $response) { $this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89); } @@ -279,34 +212,20 @@ private function extracRequestTokenFromResponse(ResponseInterface $response) { public function loggingInUsingWebAs($user) { $baseUrl = substr($this->baseUrl, 0, -5); $loginUrl = $baseUrl . '/index.php/login'; + // Request a new session and extract CSRF token - $client = new Client(); - $response = $client->get( - $loginUrl, - [ - 'cookies' => $this->cookieJar, - ] - ); - $this->extracRequestTokenFromResponse($response); + $client = $this->getGuzzleClient(null); + $response = $client->get($loginUrl, [ 'cookies' => $this->cookieJar ]); + $this->extractRequestTokenFromResponse($response); // Login and extract new token + $client = $this->getGuzzleClient(null); $password = ($user === 'admin') ? 'admin' : '123456'; - $client = new Client(); - $response = $client->post( - $loginUrl, - [ - 'form_params' => [ - 'user' => $user, - 'password' => $password, - 'requesttoken' => $this->requestToken, - ], - 'cookies' => $this->cookieJar, - 'headers' => [ - 'Origin' => $baseUrl, - ], - ] - ); - $this->extracRequestTokenFromResponse($response); + $options['form_params'] = [ 'user' => $user, 'password' => $password, 'requesttoken' => $this->requestToken ]; + $options['cookies'] = $this->cookieJar; + $options['headers'] = [ 'Origin' => $baseUrl ]; + $response = $client->post($loginUrl, $options); + $this->extractRequestTokenFromResponse($response); } /** @@ -317,31 +236,16 @@ public function loggingInUsingWebAs($user) { */ public function sendingAToWithRequesttoken($method, $url, $body = null) { $baseUrl = substr($this->baseUrl, 0, -5); - - $options = [ - 'cookies' => $this->cookieJar, - 'headers' => [ - 'requesttoken' => $this->requestToken - ], - ]; - + $fullUrl = $baseUrl . $url; + $client = $this->getGuzzleClient(null); + $options['cookies'] = $this->cookieJar; + $options['headers'] = [ 'requesttoken' => $this->requestToken ]; if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - } elseif ($body) { + $options['form_params'] = $body->getRowsHash(); + } elseif (is_array($body)) { $options = array_merge_recursive($options, $body); } - - $client = new Client(); - try { - $this->response = $client->request( - $method, - $baseUrl . $url, - $options - ); - } catch (ClientException $e) { - $this->response = $e->getResponse(); - } + $this->response = $client->request($method, $fullUrl, $options); } /** @@ -351,19 +255,10 @@ public function sendingAToWithRequesttoken($method, $url, $body = null) { */ public function sendingAToWithoutRequesttoken($method, $url) { $baseUrl = substr($this->baseUrl, 0, -5); - - $client = new Client(); - try { - $this->response = $client->request( - $method, - $baseUrl . $url, - [ - 'cookies' => $this->cookieJar - ] - ); - } catch (ClientException $e) { - $this->response = $e->getResponse(); - } + $fullUrl = $baseUrl . $url; + $client = $this->getGuzzleClient(null); + $options['cookies'] = $this->cookieJar; + $this->response = $client->request($method, $fullUrl, $options); } public static function removeFile($path, $filename) { From bed0c32f51d901fe94dd17913abd8198087384ce Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 23:10:17 -0400 Subject: [PATCH 12/27] test(integration): refactor CollaborationContext to use new Guzzle helper Signed-off-by: Josh --- .../bootstrap/CollaborationContext.php | 77 ++++--------------- 1 file changed, 13 insertions(+), 64 deletions(-) diff --git a/build/integration/features/bootstrap/CollaborationContext.php b/build/integration/features/bootstrap/CollaborationContext.php index 3b17eb62b6f3a..3f07c949975e2 100644 --- a/build/integration/features/bootstrap/CollaborationContext.php +++ b/build/integration/features/bootstrap/CollaborationContext.php @@ -8,7 +8,6 @@ */ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; -use GuzzleHttp\Client; use PHPUnit\Framework\Assert; require __DIR__ . '/autoload.php'; @@ -66,23 +65,14 @@ private function getAutocompleteWithType(int $type, string $search, TableNode $f /** * @Given /^there is a contact in an addressbook$/ */ - public function thereIsAContactInAnAddressbook() { + public function thereIsAContactInAnAddressbook(): void { $this->usingNewDavPath(); - try { - $destination = '/users/admin/myaddressbook'; - $data = 'myaddressbook'; - $this->response = $this->makeDavRequest($this->currentUser, 'MKCOL', $destination, ['Content-Type' => 'application/xml'], $data, 'addressbooks'); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $destination = '/users/admin/myaddressbook'; + $data = 'myaddressbook'; + $this->response = $this->makeDavRequest($this->currentUser, 'MKCOL', $destination, ['Content-Type' => 'application/xml'], $data, 'addressbooks'); - try { - $destination = '/users/admin/myaddressbook/contact1.vcf'; - $data = <<response = $this->makeDavRequest($this->currentUser, 'PUT', $destination, [], $data, 'addressbooks'); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 5xx responses cause a server exception - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx responses cause a client exception - $this->response = $e->getResponse(); - } + $this->response = $this->makeDavRequest($this->currentUser, 'PUT', $destination, [], $data, 'addressbooks'); } protected function resetAppConfigs(): void { @@ -116,27 +99,12 @@ protected function resetAppConfigs(): void { /** * @Given /^user "([^"]*)" has status "([^"]*)"$/ - * @param string $user - * @param string $status */ - public function assureUserHasStatus($user, $status) { + public function assureUserHasStatus(string $user, string $status) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/user_status/status"; - $client = new Client(); - $options = [ - 'headers' => [ - 'OCS-APIREQUEST' => 'true', - ], - ]; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user, $this->regularUser]; - } - - $options['form_params'] = [ - 'statusType' => $status - ]; - + $client = this->getGuzzleClient($user); + $options['headers'] = [ 'OCS-APIREQUEST' => 'true' ]; + $options['form_params'] = ['statusType' => $status ]; $this->response = $client->put($fullUrl, $options); $this->theHTTPStatusCodeShouldBe(200); @@ -144,29 +112,14 @@ public function assureUserHasStatus($user, $status) { unset($options['form_params']); $this->response = $client->get($fullUrl, $options); $this->theHTTPStatusCodeShouldBe(200); - $returnedStatus = json_decode(json_encode(simplexml_load_string($this->response->getBody()->getContents())->data), true)['status']; Assert::assertEquals($status, $returnedStatus); } - /** - * @param string $user - * @return null|array - */ public function getStatusList(string $user): ?array { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/statuses"; - $client = new Client(); - $options = [ - 'headers' => [ - 'OCS-APIREQUEST' => 'true', - ], - ]; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user, $this->regularUser]; - } - + $client = this->getGuzzleClient($user); + $options['headers'] = [ 'OCS-APIREQUEST' => 'true' ];; $this->response = $client->get($fullUrl, $options); $this->theHTTPStatusCodeShouldBe(200); @@ -176,9 +129,6 @@ public function getStatusList(string $user): ?array { /** * @Given /^user statuses for "([^"]*)" list "([^"]*)" with status "([^"]*)"$/ - * @param string $user - * @param string $statusUser - * @param string $status */ public function assertStatusesList(string $user, string $statusUser, string $status): void { $statusList = $this->getStatusList($user); @@ -197,7 +147,6 @@ public function assertStatusesList(string $user, string $statusUser, string $sta /** * @Given /^user statuses for "([^"]*)" are empty$/ - * @param string $user */ public function assertStatusesEmpty(string $user): void { $statusList = $this->getStatusList($user); From 31a6fbabab1b5f5138f3979554df75ac8ec57e32 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 23:14:01 -0400 Subject: [PATCH 13/27] test(integraiton): drop boilerplate from FilesRemindersContext Signed-off-by: Josh --- .../integration/features/bootstrap/FilesRemindersContext.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build/integration/features/bootstrap/FilesRemindersContext.php b/build/integration/features/bootstrap/FilesRemindersContext.php index ba2ea5784d336..4f38adb6014df 100644 --- a/build/integration/features/bootstrap/FilesRemindersContext.php +++ b/build/integration/features/bootstrap/FilesRemindersContext.php @@ -26,7 +26,6 @@ public function settingAReminderForFileWithDueDate($path, $dueDate) { 'PUT', '/apps/files_reminders/api/v1/' . $fileId, ['dueDate' => $dueDate], - ['OCS-APIREQUEST' => 'true'] ); } @@ -39,7 +38,6 @@ public function retrievingTheReminderForFile($path, $dueDate) { 'GET', '/apps/files_reminders/api/v1/' . $fileId, null, - ['OCS-APIREQUEST' => 'true'] ); $response = $this->getDueDateFromOCSResponse(); Assert::assertEquals($dueDate, $response); @@ -54,7 +52,6 @@ public function retrievingTheReminderForFileIsNotSet($path) { 'GET', '/apps/files_reminders/api/v1/' . $fileId, null, - ['OCS-APIREQUEST' => 'true'] ); $response = $this->getDueDateFromOCSResponse(); Assert::assertNull($response); @@ -69,7 +66,6 @@ public function removingTheReminderForFile($path) { 'DELETE', '/apps/files_reminders/api/v1/' . $fileId, null, - ['OCS-APIREQUEST' => 'true'] ); } From 2a5d8599d16b04f94e3004cc21a76fcfa9cd0238 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 23:31:05 -0400 Subject: [PATCH 14/27] test(integration): refactor Auth to use new Guzzle helpers Signed-off-by: Josh --- build/integration/features/bootstrap/Auth.php | 127 ++++++------------ 1 file changed, 42 insertions(+), 85 deletions(-) diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php index 13be6fa9cc323..f49f11a3549ae 100644 --- a/build/integration/features/bootstrap/Auth.php +++ b/build/integration/features/bootstrap/Auth.php @@ -5,26 +5,19 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ -use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ServerException; require __DIR__ . '/autoload.php'; trait Auth { - /** @var string */ - private $unrestrictedClientToken; - /** @var string */ - private $restrictedClientToken; - /** @var Client */ - private $client; - /** @var string */ - private $responseXml; + private string $unrestrictedClientToken; + private string $restrictedClientToken; + private Client $client; + private string $responseXml; /** @BeforeScenario */ public function setUpScenario() { - $this->client = new Client(); + $this->client = $this->getGuzzleClient(null); $this->responseXml = ''; $this->cookieJar = new CookieJar(); } @@ -38,27 +31,15 @@ public function requestingWith($url, $method) { private function sendRequest($url, $method, $authHeader = null, $useCookies = false) { $fullUrl = substr($this->baseUrl, 0, -5) . $url; - try { - if ($useCookies) { - $options = [ - 'cookies' => $this->cookieJar, - ]; - } else { - $options = []; - } - if ($authHeader) { - $options['headers'] = [ - 'Authorization' => $authHeader - ]; - } - $options['headers']['OCS_APIREQUEST'] = 'true'; - $options['headers']['requesttoken'] = $this->requestToken; - $this->response = $this->client->request($method, $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } catch (ServerException $ex) { - $this->response = $ex->getResponse(); + if ($useCookies) { + $options['cookies'] = $this->cookieJar; } + if ($authHeader) { + $options['headers'] = [ 'Authorization' => $authHeader ]; + } + $options['headers']['OCS_APIREQUEST'] = 'true'; + $options['headers']['requesttoken'] = $this->requestToken; + $this->response = $this->client->request($method, $fullUrl, $options); } /** @@ -69,33 +50,20 @@ public function theCsrfTokenIsExtractedFromThePreviousResponse() { } /** - * @param bool $loginViaWeb * @return object */ - private function createClientToken($loginViaWeb = true) { + private function createClientToken(bool $loginViaWeb = true) { if ($loginViaWeb) { $this->loggingInUsingWebAs('user0'); } $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens'; - $client = new Client(); - $options = [ - 'auth' => [ - 'user0', - $loginViaWeb ? '123456' : $this->restrictedClientToken, - ], - 'form_params' => [ - 'requesttoken' => $this->requestToken, - 'name' => md5(microtime()), - ], - 'cookies' => $this->cookieJar, - ]; + $client = $this->getGuzzleClient(null); + $options['auth'] => [ 'user0', $loginViaWeb ? '123456' : $this->restrictedClientToken ]; + $options['form_params'] = [ 'requesttoken' => $this->requestToken, 'name' => md5(microtime()) ]; + $options['cookies'] = $this->cookieJar; - try { - $this->response = $client->request('POST', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ServerException $e) { - $this->response = $e->getResponse(); - } + $this->response = $client->request('POST', $fullUrl, $options); return json_decode($this->response->getBody()->getContents()); } @@ -106,20 +74,15 @@ public function aNewRestrictedClientTokenIsAdded() { $tokenObj = $this->createClientToken(); $newCreatedTokenId = $tokenObj->deviceToken->id; $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens/' . $newCreatedTokenId; - $client = new Client(); - $options = [ - 'auth' => ['user0', '123456'], - 'headers' => [ - 'requesttoken' => $this->requestToken, - ], - 'json' => [ - 'name' => md5(microtime()), - 'scope' => [ - 'filesystem' => false, - ], - ], - 'cookies' => $this->cookieJar, + $client = $this->getGuzzleClient(null); + $options['auth'] = [ 'user0', '123456' ]; + $options['headers'] = [ 'requesttoken' => $this->requestToken ]; + $options['json'] = [ + 'name' => md5(microtime()), + 'scope' => [ 'filesystem' => false ], ]; + $options['cookies'] = $this->cookieJar; + $this->response = $client->request('PUT', $fullUrl, $options); $this->restrictedClientToken = $tokenObj->token; } @@ -207,29 +170,23 @@ public function aNewBrowserSessionIsStarted($remember = false) { $baseUrl = substr($this->baseUrl, 0, -5); $loginUrl = $baseUrl . '/login'; // Request a new session and extract CSRF token - $client = new Client(); - $response = $client->get($loginUrl, [ - 'cookies' => $this->cookieJar, - ]); - $this->extracRequestTokenFromResponse($response); + $client = $this->getGuzzleClient(null); + $options['cookies'] = $this->cookieJar; + $response = $client->get($loginUrl, $options); + $this->extractRequestTokenFromResponse($response); // Login and extract new token - $client = new Client(); - $response = $client->post( - $loginUrl, [ - 'form_params' => [ - 'user' => 'user0', - 'password' => '123456', - 'rememberme' => $remember ? '1' : '0', - 'requesttoken' => $this->requestToken, - ], - 'cookies' => $this->cookieJar, - 'headers' => [ - 'Origin' => $baseUrl, - ], - ] - ); - $this->extracRequestTokenFromResponse($response); + $client = $this->getGuzzleClient(null); + $options['form_params'] = [ + 'user' => 'user0', + 'password' => '123456', + 'rememberme' => $remember ? '1' : '0', + 'requesttoken' => $this->requestToken, + ]; + $options['cookies'] = $this->cookieJar; + $options['headers'] = [ 'Origin' => $baseUrl ]; + $response = $client->post($loginUrl, $options); + $this->extractRequestTokenFromResponse($response); } /** From 1ea3e3e401792721a5d0c46b8b6e13bd7c00cf37 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 23:55:33 -0400 Subject: [PATCH 15/27] test(integration): fixup Auth Signed-off-by: Josh --- build/integration/features/bootstrap/Auth.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php index f49f11a3549ae..80e6a6e380939 100644 --- a/build/integration/features/bootstrap/Auth.php +++ b/build/integration/features/bootstrap/Auth.php @@ -5,15 +5,16 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ +use GuzzleHttp\Client as GClient; use GuzzleHttp\Cookie\CookieJar; require __DIR__ . '/autoload.php'; trait Auth { - private string $unrestrictedClientToken; - private string $restrictedClientToken; - private Client $client; - private string $responseXml; + private ?string $unrestrictedClientToken; + private ?string $restrictedClientToken; + private ?GClient $client; + private ?string $responseXml; /** @BeforeScenario */ public function setUpScenario() { @@ -56,13 +57,11 @@ private function createClientToken(bool $loginViaWeb = true) { if ($loginViaWeb) { $this->loggingInUsingWebAs('user0'); } - $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens'; $client = $this->getGuzzleClient(null); - $options['auth'] => [ 'user0', $loginViaWeb ? '123456' : $this->restrictedClientToken ]; + $options['auth'] = [ 'user0', $loginViaWeb ? '123456' : $this->restrictedClientToken ]; $options['form_params'] = [ 'requesttoken' => $this->requestToken, 'name' => md5(microtime()) ]; $options['cookies'] = $this->cookieJar; - $this->response = $client->request('POST', $fullUrl, $options); return json_decode($this->response->getBody()->getContents()); } @@ -82,7 +81,6 @@ public function aNewRestrictedClientTokenIsAdded() { 'scope' => [ 'filesystem' => false ], ]; $options['cookies'] = $this->cookieJar; - $this->response = $client->request('PUT', $fullUrl, $options); $this->restrictedClientToken = $tokenObj->token; } From d059fd1cd12c2cc31816d70a3bf1b7c125a70d68 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 11 Apr 2026 23:58:48 -0400 Subject: [PATCH 16/27] test(integration): fixup BasicStructure Signed-off-by: Josh --- build/integration/features/bootstrap/BasicStructure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index bdf4120d9c8b2..f493e86c0bb5b 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -23,7 +23,7 @@ trait BasicStructure { private string $currentServer = ''; private string $baseUrl = ''; private int $apiVersion = 1; - private ResponseInterface $response = null; + private ?ResponseInterface $response = null; private CookieJar $cookieJar; private string $requestToken; From c1d7164a49dbf85fbb142bfff9686471160ab993 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 00:02:47 -0400 Subject: [PATCH 17/27] test(integration): drop var_dump from WebDav Signed-off-by: Josh --- build/integration/features/bootstrap/WebDav.php | 1 - 1 file changed, 1 deletion(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index fbed1d04ee796..8ccc31d16c0cb 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -610,7 +610,6 @@ public function searchFile(string $user, ?string $properties = null, ?string $sc '; $this->response = $this->makeDavRequest($user, 'SEARCH', '', ['Content-Type' => 'text/xml'], $body, ''); - var_dump((string)$this->response->getBody()); } /* Returns the elements of a report command From 9cc4f37b6f33aee828416246f2be0ba176a52757 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 08:37:15 -0400 Subject: [PATCH 18/27] test(integration): refactor/align property typing in Sharing Signed-off-by: Josh --- build/integration/features/bootstrap/Sharing.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index aba3d3090beca..41dad1a3ddd3a 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -23,8 +23,7 @@ trait Sharing { /** @var SimpleXMLElement[] */ private array $storedShareData = []; private ?string $savedShareId = null; - /** @var ResponseInterface */ - private $response; + private ?ResponseInterface $response = null; /** * @Given /^as "([^"]*)" creating a share with$/ From a7f4dda8feca07588c457725e1b9c4ea236c822f Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 08:58:15 -0400 Subject: [PATCH 19/27] test(integration): refactor Provisioning to use helpers / streamline Signed-off-by: Josh --- .../features/bootstrap/Provisioning.php | 408 +++++------------- 1 file changed, 105 insertions(+), 303 deletions(-) diff --git a/build/integration/features/bootstrap/Provisioning.php b/build/integration/features/bootstrap/Provisioning.php index c539e4e9b53d5..77a854c6391b3 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -7,7 +7,6 @@ */ use Behat\Gherkin\Node\TableNode; -use GuzzleHttp\Client; use GuzzleHttp\Message\ResponseInterface; use PHPUnit\Framework\Assert; @@ -47,14 +46,44 @@ public function restoreAppsEnabledStateAfterScenario() { } } + /** + * Sends an authenticated OCS request using the centralized Guzzle client. + * + * @param string $method HTTP method (GET, POST, PUT, DELETE) + * @param string $url Full URL to request + * @param array $additionalOptions Extra Guzzle request options (form_params, headers to merge, etc.) + * @return \Psr\Http\Message\ResponseInterface + */ + private function sendOcsRequest(string $method, string $url, array $additionalOptions = []): \Psr\Http\Message\ResponseInterface { + $client = $this->getGuzzleClient($this->currentUser); + $options = array_merge_recursive([ + 'headers' => [ + 'OCS-APIREQUEST' => 'true', + ], + ], $additionalOptions); + return $client->request($method, $url, $options); + } + + /** + * Sends an authenticated OCS request, always as admin. + */ + private function sendOcsRequestAsAdmin(string $method, string $url, array $additionalOptions = []): \Psr\Http\Message\ResponseInterface { + $client = $this->getGuzzleClient('admin'); + $options = array_merge_recursive([ + 'headers' => [ + 'OCS-APIREQUEST' => 'true', + ], + ], $additionalOptions); + return $client->request($method, $url, $options); + } + /** * @Given /^user "([^"]*)" exists$/ * @param string $user */ public function assureUserExists($user) { - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { + $this->userExists($user); + if ($this->response->getStatusCode() !== 200) { $previous_user = $this->currentUser; $this->currentUser = 'admin'; $this->creatingTheUser($user); @@ -69,9 +98,8 @@ public function assureUserExists($user) { * @param string $user */ public function assureUserWithDisplaynameExists($user, $displayname) { - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { + $this->userExists($user); + if ($this->response->getStatusCode() !== 200) { $previous_user = $this->currentUser; $this->currentUser = 'admin'; $this->creatingTheUser($user, $displayname); @@ -86,60 +114,45 @@ public function assureUserWithDisplaynameExists($user, $displayname) { * @param string $user */ public function userDoesNotExist($user) { - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); + $this->userExists($user); + if ($this->response->getStatusCode() === 404) { return; } $previous_user = $this->currentUser; $this->currentUser = 'admin'; $this->deletingTheUser($user); $this->currentUser = $previous_user; - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); - } + $this->userExists($user); + Assert::assertEquals(404, $this->response->getStatusCode()); } public function creatingTheUser($user, $displayname = '') { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - - $options['form_params'] = [ + $formParams = [ 'userid' => $user, 'password' => '123456' ]; if ($displayname !== '') { - $options['form_params']['displayName'] = $displayname; + $formParams['displayName'] = $displayname; } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->post($fullUrl, $options); + $this->response = $this->sendOcsRequest('POST', $fullUrl, [ + 'form_params' => $formParams, + ]); if ($this->currentServer === 'LOCAL') { $this->createdUsers[$user] = $user; } elseif ($this->currentServer === 'REMOTE') { $this->createdRemoteUsers[$user] = $user; } - //Quick hack to login once with the current user - $options2 = [ - 'auth' => [$user, '123456'], - ]; - $options2['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; + // Quick hack to login once with the current user + $client = $this->getGuzzleClient(null); $url = $fullUrl . '/' . $user; - $client->get($url, $options2); + $client->get($url, [ + 'auth' => [$user, '123456'], + 'headers' => [ + 'OCS-APIREQUEST' => 'true', + ], + ]); } /** @@ -150,18 +163,7 @@ public function creatingTheUser($user, $displayname = '') { */ public function userHasSetting($user, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $response = $client->get($fullUrl, $options); + $response = $this->sendOcsRequest('GET', $fullUrl); foreach ($settings->getRows() as $setting) { $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1); if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) { @@ -182,19 +184,11 @@ public function userHasSetting($user, $settings) { */ public function userHasProfileData(string $user, ?TableNode $settings): void { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/profile/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - 'Accept' => 'application/json', - ]; - - $response = $client->get($fullUrl, $options); + $response = $this->sendOcsRequest('GET', $fullUrl, [ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); $body = $response->getBody()->getContents(); $data = json_decode($body, true); $data = $data['ocs']['data']; @@ -211,23 +205,12 @@ public function userHasProfileData(string $user, ?TableNode $settings): void { /** * @Then /^group "([^"]*)" has$/ * - * @param string $user + * @param string $group * @param TableNode|null $settings */ public function groupHasSetting($group, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $response = $client->get($fullUrl, $options); + $response = $this->sendOcsRequest('GET', $fullUrl); $groupDetails = simplexml_load_string($response->getBody())->data[0]->groups[0]->element; foreach ($settings->getRows() as $setting) { $value = json_decode(json_encode($groupDetails->{$setting[0]}), 1); @@ -251,18 +234,7 @@ public function userHasEditableFields($user, $fields) { if ($user !== 'self') { $fullUrl .= '/' . $user; } - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $response = $client->get($fullUrl, $options); + $response = $this->sendOcsRequest('GET', $fullUrl); $fieldsArray = json_decode(json_encode(simplexml_load_string($response->getBody())->data->element), 1); $expectedFields = $fields->getRows(); @@ -276,17 +248,11 @@ public function userHasEditableFields($user, $fields) { /** * @Then /^search users by phone for region "([^"]*)" with$/ * - * @param string $user - * @param TableNode|null $settings + * @param string $region + * @param TableNode $searchTable */ public function searchUserByPhone($region, TableNode $searchTable) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; $search = []; foreach ($searchTable->getRows() as $row) { @@ -296,12 +262,12 @@ public function searchUserByPhone($region, TableNode $searchTable) { $search[$row[0]][] = $row[1]; } - $options['form_params'] = [ - 'location' => $region, - 'search' => $search, - ]; - - $this->response = $client->post($fullUrl, $options); + $this->response = $this->sendOcsRequestAsAdmin('POST', $fullUrl, [ + 'form_params' => [ + 'location' => $region, + 'search' => $search, + ], + ]); } public function createUser($user) { @@ -338,14 +304,7 @@ public function deleteGroup($group) { public function userExists($user) { $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true' - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequestAsAdmin('GET', $fullUrl); } /** @@ -355,16 +314,7 @@ public function userExists($user) { */ public function checkThatUserBelongsToGroup($user, $group) { $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); $respondedArray = $this->getArrayOfGroupsResponded($this->response); sort($respondedArray); Assert::assertContains($group, $respondedArray); @@ -373,16 +323,7 @@ public function checkThatUserBelongsToGroup($user, $group) { public function userBelongsToGroup($user, $group) { $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); $respondedArray = $this->getArrayOfGroupsResponded($this->response); if (array_key_exists($group, $respondedArray)) { @@ -416,16 +357,7 @@ public function assureUserBelongsToGroup($user, $group) { */ public function userDoesNotBelongToGroup($user, $group) { $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); $groups = [$group]; $respondedArray = $this->getArrayOfGroupsResponded($this->response); Assert::assertNotEqualsCanonicalizing($groups, $respondedArray); @@ -438,20 +370,11 @@ public function userDoesNotBelongToGroup($user, $group) { */ public function creatingTheGroup($group) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - - $options['form_params'] = [ - 'groupid' => $group, - ]; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->post($fullUrl, $options); + $this->response = $this->sendOcsRequest('POST', $fullUrl, [ + 'form_params' => [ + 'groupid' => $group, + ], + ]); if ($this->currentServer === 'LOCAL') { $this->createdGroups[$group] = $group; } elseif ($this->currentServer === 'REMOTE') { @@ -464,20 +387,7 @@ public function creatingTheGroup($group) { */ public function assureUserIsDisabled($user) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/disable"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - // TODO: fix hack - $options['form_params'] = [ - 'foo' => 'bar' - ]; - - $this->response = $client->put($fullUrl, $options); + $this->response = $this->sendOcsRequest('PUT', $fullUrl); } /** @@ -486,16 +396,7 @@ public function assureUserIsDisabled($user) { */ public function deletingTheUser($user) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->delete($fullUrl, $options); + $this->response = $this->sendOcsRequest('DELETE', $fullUrl); } /** @@ -504,16 +405,7 @@ public function deletingTheUser($user) { */ public function deletingTheGroup($group) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/$group"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->delete($fullUrl, $options); + $this->response = $this->sendOcsRequest('DELETE', $fullUrl); if ($this->currentServer === 'LOCAL') { unset($this->createdGroups[$group]); @@ -540,33 +432,17 @@ public function addUserToGroup($user, $group) { */ public function addingUserToGroup($user, $group) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $options['form_params'] = [ - 'groupid' => $group, - ]; - - $this->response = $client->post($fullUrl, $options); + $this->response = $this->sendOcsRequest('POST', $fullUrl, [ + 'form_params' => [ + 'groupid' => $group, + ], + ]); } public function groupExists($group) { $fullUrl = $this->baseUrl . "v2.php/cloud/groups/$group"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequestAsAdmin('GET', $fullUrl); } /** @@ -574,9 +450,8 @@ public function groupExists($group) { * @param string $group */ public function assureGroupExists($group) { - try { - $this->groupExists($group); - } catch (\GuzzleHttp\Exception\ClientException $ex) { + $this->groupExists($group); + if ($this->response->getStatusCode() !== 200) { $previous_user = $this->currentUser; $this->currentUser = 'admin'; $this->creatingTheGroup($group); @@ -591,23 +466,16 @@ public function assureGroupExists($group) { * @param string $group */ public function groupDoesNotExist($group) { - try { - $this->groupExists($group); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); + $this->groupExists($group); + if ($this->response->getStatusCode() === 404) { return; } $previous_user = $this->currentUser; $this->currentUser = 'admin'; $this->deletingTheGroup($group); $this->currentUser = $previous_user; - try { - $this->groupExists($group); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); - } + $this->groupExists($group); + Assert::assertEquals(404, $this->response->getStatusCode()); } /** @@ -617,16 +485,7 @@ public function groupDoesNotExist($group) { */ public function userIsSubadminOfGroup($user, $group) { $fullUrl = $this->baseUrl . "v2.php/cloud/groups/$group/subadmins"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); sort($respondedArray); Assert::assertContains($user, $respondedArray); @@ -640,18 +499,11 @@ public function userIsSubadminOfGroup($user, $group) { */ public function assureUserIsSubadminOfGroup($user, $group) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/subadmins"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['form_params'] = [ - 'groupid' => $group - ]; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - $this->response = $client->post($fullUrl, $options); + $this->response = $this->sendOcsRequest('POST', $fullUrl, [ + 'form_params' => [ + 'groupid' => $group, + ], + ]); Assert::assertEquals(200, $this->response->getStatusCode()); } @@ -662,16 +514,7 @@ public function assureUserIsSubadminOfGroup($user, $group) { */ public function userIsNotSubadminOfGroup($user, $group) { $fullUrl = $this->baseUrl . "v2.php/cloud/groups/$group/subadmins"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); sort($respondedArray); Assert::assertNotContains($user, $respondedArray); @@ -847,16 +690,7 @@ public function appEnabledStateWillBeRestoredOnceTheScenarioFinishes($app) { private function getAppsWithFilter($filter) { $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=' . $filter; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); return $this->getArrayOfAppsResponded($this->response); } @@ -899,16 +733,7 @@ public function appIsNotEnabled($app) { */ public function userIsDisabled($user) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); // false in xml is empty Assert::assertTrue(empty(simplexml_load_string($this->response->getBody())->data[0]->enabled)); } @@ -919,16 +744,7 @@ public function userIsDisabled($user) { */ public function userIsEnabled($user) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequest('GET', $fullUrl); // boolean to string is integer Assert::assertEquals('1', simplexml_load_string($this->response->getBody())->data[0]->enabled); } @@ -963,10 +779,7 @@ public function userHasUnlimitedQuota($user) { */ public function getUserHome($user) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $this->response = $client->get($fullUrl, $options); + $this->response = $this->sendOcsRequestAsAdmin('GET', $fullUrl); return simplexml_load_string($this->response->getBody())->data[0]->home; } @@ -1009,18 +822,7 @@ public function cleanupGroups() { */ public function userHasNotSetting($user, TableNode $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $response = $client->get($fullUrl, $options); + $response = $this->sendOcsRequest('GET', $fullUrl); foreach ($settings->getRows() as $setting) { $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1); if (isset($value[0])) { From ce2db7cf088b16b0edfbc9e80e242faf9938e1ab Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:06:32 -0400 Subject: [PATCH 20/27] test(integration): align/modernize property typing in BasicStructure Signed-off-by: Josh --- build/integration/features/bootstrap/BasicStructure.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index f493e86c0bb5b..18b8746565021 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -27,10 +27,10 @@ trait BasicStructure { private CookieJar $cookieJar; private string $requestToken; - protected $adminUser; - protected $regularUser; - protected $localBaseUrl; - protected $remoteBaseUrl; + protected string $adminUser = ''; + protected string $regularUser = ''; + protected string $localBaseUrl = ''; + protected string $remoteBaseUrl = ''; public function __construct($baseUrl, $admin, $regular_user_password) { // Initialize your context here From b9619118c8639b6b42d61b4db37f8c127344931b Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:10:30 -0400 Subject: [PATCH 21/27] test(integration): add inline typing to Mail properties Signed-off-by: Josh --- build/integration/features/bootstrap/Mail.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php index d48ed6399c521..fddc37c8ebb90 100644 --- a/build/integration/features/bootstrap/Mail.php +++ b/build/integration/features/bootstrap/Mail.php @@ -8,10 +8,7 @@ trait Mail { // CommandLine trait is expected to be used in the class that uses this // trait. - /** - * @var string - */ - private $fakeSmtpServerPid; + private ?string $fakeSmtpServerPid = null; /** * @AfterScenario From ae844c7f87b50fc72e118dbb33b784559b63c19d Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:14:28 -0400 Subject: [PATCH 22/27] test(integration): align WebDav response property typing Signed-off-by: Josh --- build/integration/features/bootstrap/WebDav.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 8ccc31d16c0cb..4ab9138c838db 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -22,8 +22,7 @@ trait WebDav { private bool $usingOldDavPath = true; private ?array $storedETAG = null; // map with user as key and another map as value, which has path as key and etag as value private ?int $storedFileID = null; - /** @var ResponseInterface */ - private $response; + private ?RespondInterface $response = null; private array $parsedResponse = []; private string $s3MultipartDestination; private string $uploadId; From 8f1813711f2bd8d86d134a05ebe76252e62d37ce Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:19:01 -0400 Subject: [PATCH 23/27] test(integration): fixup WebDav typo Signed-off-by: Josh --- build/integration/features/bootstrap/WebDav.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 4ab9138c838db..2efa469474df1 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -22,7 +22,7 @@ trait WebDav { private bool $usingOldDavPath = true; private ?array $storedETAG = null; // map with user as key and another map as value, which has path as key and etag as value private ?int $storedFileID = null; - private ?RespondInterface $response = null; + private ?ResponseInterface $response = null; private array $parsedResponse = []; private string $s3MultipartDestination; private string $uploadId; From 75cdc06dacd7c62d98e807ab888cb0ca30e8a29a Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:23:21 -0400 Subject: [PATCH 24/27] test(integration): align/modernize AppConfiguration properties Signed-off-by: Josh --- build/integration/features/bootstrap/AppConfiguration.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build/integration/features/bootstrap/AppConfiguration.php b/build/integration/features/bootstrap/AppConfiguration.php index 1313e3421072d..0606729b030b9 100644 --- a/build/integration/features/bootstrap/AppConfiguration.php +++ b/build/integration/features/bootstrap/AppConfiguration.php @@ -14,11 +14,9 @@ require __DIR__ . '/autoload.php'; trait AppConfiguration { - /** @var string */ - private $currentUser = ''; - /** @var ResponseInterface */ - private $response = null; + private string $currentUser = ''; + private ?ResponseInterface $response = null; abstract public function sendingTo(string $verb, string $url); abstract public function sendingToWith(string $verb, string $url, ?TableNode $body); From 86cac2330c660f50f41b8ca80a6264885a1720ce Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:39:33 -0400 Subject: [PATCH 25/27] test(integration): fixup typing alignment in BasicStructure Signed-off-by: Josh --- build/integration/features/bootstrap/BasicStructure.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index 18b8746565021..342d1dd309996 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -27,12 +27,13 @@ trait BasicStructure { private CookieJar $cookieJar; private string $requestToken; - protected string $adminUser = ''; - protected string $regularUser = ''; + /** @var array{0:string,1:string} */ + protected array $adminUser; + protected string $regularUser; protected string $localBaseUrl = ''; protected string $remoteBaseUrl = ''; - public function __construct($baseUrl, $admin, $regular_user_password) { + public function __construct(string $baseUrl, array $admin, string $regular_user_password) { // Initialize your context here $this->baseUrl = $baseUrl; $this->adminUser = $admin; From ad4ccc22266f70c8b889e6d836dbb7e957e22ae1 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 09:45:12 -0400 Subject: [PATCH 26/27] test(integration): add "anonymous" user support to auth helper Signed-off-by: Josh --- build/integration/features/bootstrap/WebDav.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 2efa469474df1..ce5f99c9dcadc 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -105,7 +105,7 @@ private function getDavUrl(?string $user, string $path, string $type = 'files'): private function getAuthForUser(?string $user): ?array { if ($user === 'admin') { return $this->adminUser; - } elseif ($user !== null && $user !== '') { + } elseif ($user !== null && $user !== '' && !str_starts_with($user, 'anonymous')) { return [$user, $this->regularUser]; } From a12b7cc2bc4b8bb769a39d5c5bc32c0fa7689a6f Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 12 Apr 2026 12:36:12 -0400 Subject: [PATCH 27/27] test(integration): fixup typos in CollaborationContext Signed-off-by: Josh --- .../integration/features/bootstrap/CollaborationContext.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/integration/features/bootstrap/CollaborationContext.php b/build/integration/features/bootstrap/CollaborationContext.php index 3f07c949975e2..233a7ca54c8b9 100644 --- a/build/integration/features/bootstrap/CollaborationContext.php +++ b/build/integration/features/bootstrap/CollaborationContext.php @@ -102,7 +102,7 @@ protected function resetAppConfigs(): void { */ public function assureUserHasStatus(string $user, string $status) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/user_status/status"; - $client = this->getGuzzleClient($user); + $client = $this->getGuzzleClient($user); $options['headers'] = [ 'OCS-APIREQUEST' => 'true' ]; $options['form_params'] = ['statusType' => $status ]; $this->response = $client->put($fullUrl, $options); @@ -118,8 +118,8 @@ public function assureUserHasStatus(string $user, string $status) { public function getStatusList(string $user): ?array { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/statuses"; - $client = this->getGuzzleClient($user); - $options['headers'] = [ 'OCS-APIREQUEST' => 'true' ];; + $client = $this->getGuzzleClient($user); + $options['headers'] = [ 'OCS-APIREQUEST' => 'true' ]; $this->response = $client->get($fullUrl, $options); $this->theHTTPStatusCodeShouldBe(200);