test(api): update php test dependencies (#3335)

* test: update php dependencies

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: use custom tus client

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: fix php code style and commands

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: use doctrine coding standard

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: convert headers value to string

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: initialize phpunit config

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: fix tus upload

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: fix php code style

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

---------

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>
This commit is contained in:
Sawjan Gurung authored and GitHub committed 2026-08-21 19:38:34 +02:00
1 parent ec4d5b7e6b
commit cc8a161fce
12 files changed
+644 -137

No files matched your search

-1
View File
@@ -36,7 +36,6 @@ vendor-bin/**/composer.lock
vendor-php
# API acceptance tests - auto-generated files
.php-cs-fixer.cache
suite-logs
tests/acceptance/filesForUpload/filesWithVirus/
-31
View File
@@ -1,31 +0,0 @@
<?php
$dirToParse = 'tests/acceptance/';
$dirIterator = new DirectoryIterator(__DIR__ . '/' . $dirToParse);
$excludeDirs = [
'node_modules',
'vendor-php'
];
$finder = PhpCsFixer\Finder::create()
->exclude($excludeDirs)
->in(__DIR__);
$ocRule = (new OC\CodingStandard\Config())->getRules();
$config = new PhpCsFixer\Config();
$config->setFinder($finder)
->setIndent("\t")
->setRules(
array_merge(
$ocRule,
[
"return_type_declaration" => [
"space_before" => "none",
],
'single_space_around_construct' => true
]
)
);
$config->setFinder($finder);
return $config;
+16 -42
View File
@@ -920,30 +920,6 @@ def buildOpencloudBinaryForTesting(ctx):
}
return [pipeline]
def vendorbinCodestyle(phpVersion):
return [{
"name": "vendorbin-codestyle",
"image": OC_CI_PHP % phpVersion,
"environment": {
"COMPOSER_HOME": "%s/.cache/composer" % dirs["base"],
},
"commands": [
"make vendor-bin-codestyle",
],
}]
def vendorbinCodesniffer(phpVersion):
return [{
"name": "vendorbin-codesniffer",
"image": OC_CI_PHP % phpVersion,
"environment": {
"COMPOSER_HOME": "%s/.cache/composer" % dirs["base"],
},
"commands": [
"make vendor-bin-codesniffer",
],
}]
def checkTestSuitesInExpectedFailures(ctx):
return [{
"name": "check-suites-in-expected-failures",
@@ -1031,24 +1007,22 @@ def codestyle(ctx):
result = {
"name": name,
"steps": vendorbinCodestyle(phpVersion) +
vendorbinCodesniffer(phpVersion) +
[
{
"name": "php-style",
"image": OC_CI_PHP % phpVersion,
"commands": [
"make test-php-style",
],
},
{
"name": "check-env-var-annotations",
"image": OC_CI_PHP % phpVersion,
"commands": [
"make check-env-var-annotations",
],
},
],
"steps": [
{
"name": "php-style",
"image": OC_CI_PHP % phpVersion,
"commands": [
"make test-php-style",
],
},
{
"name": "check-env-var-annotations",
"image": OC_CI_PHP % phpVersion,
"commands": [
"make check-env-var-annotations",
],
},
],
"depends_on": [],
"when": [
event["base"],
+3 -21
View File
@@ -68,11 +68,8 @@ OC_MODULES = \
protogen
# bin file definitions
PHP_CS_FIXER=php -d zend.enable_gc=0 vendor-bin/opencloud-codestyle/vendor/bin/php-cs-fixer
PHP_CODESNIFFER=vendor-bin/php_codesniffer/vendor/bin/phpcs
PHP_CODEBEAUTIFIER=vendor-bin/php_codesniffer/vendor/bin/phpcbf
PHAN=php -d zend.enable_gc=0 vendor-bin/phan/vendor/bin/phan
PHPSTAN=php -d zend.enable_gc=0 vendor-bin/phpstan/vendor/bin/phpstan
PHP_CODESNIFFER=vendor-bin/opencloud-codestyle/vendor/bin/phpcs
PHP_CODEBEAUTIFIER=vendor-bin/opencloud-codestyle/vendor/bin/phpcbf
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include .bingo/Variables.mk
@@ -319,30 +316,15 @@ ci-format: $(BUILDIFIER)
$(BUILDIFIER) --mode=fix .woodpecker.star
.PHONY: test-php-style
test-php-style: vendor-bin/opencloud-codestyle/vendor vendor-bin/php_codesniffer/vendor
$(PHP_CS_FIXER) fix -v --diff --allow-risky yes --dry-run
test-php-style: vendor-bin/opencloud-codestyle/vendor
$(PHP_CODESNIFFER) --cache --runtime-set ignore_warnings_on_exit --standard=phpcs.xml tests/acceptance tests/acceptance/TestHelpers
.PHONY: test-php-style-fix
test-php-style-fix: vendor-bin/opencloud-codestyle/vendor
$(PHP_CS_FIXER) fix -v --diff --allow-risky yes
$(PHP_CODEBEAUTIFIER) --cache --runtime-set ignore_warnings_on_exit --standard=phpcs.xml tests/acceptance
.PHONY: vendor-bin-codestyle
vendor-bin-codestyle: vendor-bin/opencloud-codestyle/vendor
.PHONY: vendor-bin-codesniffer
vendor-bin-codesniffer: vendor-bin/php_codesniffer/vendor
vendor-bin/opencloud-codestyle/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/opencloud-codestyle/composer.lock
composer bin opencloud-codestyle install --no-progress
vendor-bin/opencloud-codestyle/composer.lock: vendor-bin/opencloud-codestyle/composer.json
@echo opencloud-codestyle composer.lock is not up to date.
vendor-bin/php_codesniffer/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/php_codesniffer/composer.lock
composer bin php_codesniffer install --no-progress
vendor-bin/php_codesniffer/composer.lock: vendor-bin/php_codesniffer/composer.json
@echo php_codesniffer composer.lock is not up to date.
+4 -3
View File
@@ -2,7 +2,7 @@
"name": "opencloud-eu/opencloud",
"config": {
"platform": {
"php": "8.3"
"php": "8.4"
},
"vendor-dir": "./vendor-php",
"allow-plugins": {
@@ -11,11 +11,12 @@
},
"require-dev": {
"ext-simplexml": "*",
"bamarni/composer-bin-plugin": "^1.8"
"bamarni/composer-bin-plugin": "^1.9"
},
"extra": {
"bamarni-bin": {
"bin-links": false
"bin-links": false,
"forward-command": false
}
}
}
@@ -463,6 +463,10 @@ class HttpRequestHelper {
if ($xRequestId !== '') {
$headers['X-Request-ID'] = $xRequestId;
}
// Guzzle 8 expects all header values to be strings or arrays of strings.
$headers = array_map('strval', $headers);
if (\is_array($body)) {
// When creating the client, it is possible to set 'form_params' and
// the Client constructor sorts out doing this http_build_query stuff.
+580
View File
@@ -0,0 +1,580 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2026 Sajan Gurung sajan@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
/**
* Most of the code is copied from https://github.com/ankitpokhrel/tus-php/blob/main/src/Tus/Client.php
*/
namespace TestHelpers;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\ConnectException;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
/**
* TUS client for uploading files using the TUS protocol.
*/
class TUSClient {
/**
* @const string Tus protocol version.
*/
public const TUS_PROTOCOL_VERSION = '1.0.0';
/**
* @const string Header Content Type
*/
protected const HEADER_CONTENT_TYPE = 'application/offset+octet-stream';
/**
* @const Input stream
*/
public const INPUT_STREAM = 'php://input';
/**
* @const Read binary mode
*/
public const READ_BINARY = 'rb';
/**
* @var GuzzleClient
*/
protected $client;
/**
* @var string
*/
protected $apiPath = '/files';
/**
* @var string
*/
protected $filePath;
/**
* @var int
*/
protected $fileSize = 0;
/**
* @var string
*/
protected $fileName;
/**
* @var string
*/
protected $url;
/**
* @var string
*/
protected $checksum;
/**
* @var int
*/
protected $partialOffset = -1;
/**
* @var string
*/
protected $checksumAlgorithm = 'sha256';
/**
* @var array
*/
protected $metadata = [];
/**
* @var array
*/
protected $headers = [];
/**
* Client constructor.
*
* @param string $baseUri
* @param array $options
*
* @throws \ReflectionException
*/
public function __construct(string $baseUri, array $options = []) {
$this->headers = $options['headers'] ?? [];
$options['headers'] = [
'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
] + ($this->headers);
$this->client = new GuzzleClient(
['base_uri' => $baseUri] + $options
);
}
/**
* Get guzzle client.
*
* @return GuzzleClient
*/
public function getClient(): GuzzleClient {
return $this->client;
}
/**
* Get file size.
*
* @return int
*/
public function getFileSize(): int {
return $this->fileSize;
}
/**
* Get file path.
*
* @return string|null
*/
public function getFilePath(): ?string {
return $this->filePath;
}
/**
* Get url.
*
* @return string|null
*/
public function getUrl(): ?string {
return $this->url;
}
/**
* Set API path.
*
* @param string $path
*
* @return self
*/
public function setApiPath(string $path): self {
$this->apiPath = $path;
return $this;
}
/**
* Set checksum algorithm.
*
* @param string $algorithm
*
* @return Client
*/
public function setChecksumAlgorithm(string $algorithm): self {
$this->checksumAlgorithm = $algorithm;
return $this;
}
/**
* Get checksum algorithm.
*
* @return string
*/
public function getChecksumAlgorithm(): string {
return $this->checksumAlgorithm;
}
/**
* Set checksum.
*
* @param string $checksum
*
* @return Client
*/
public function setChecksum(string $checksum): self {
$this->checksum = $checksum;
return $this;
}
/**
* Get checksum.
*
* @return string
*/
public function getChecksum(): string {
if (empty($this->checksum)) {
$this->setChecksum(hash_file($this->getChecksumAlgorithm(), $this->getFilePath()));
}
return $this->checksum;
}
/**
* Get upload checksum header.
*
* @return string
*/
protected function getUploadChecksumHeader(): string {
return $this->getChecksumAlgorithm() . ' ' . base64_encode($this->getChecksum());
}
/**
* Set metadata.
*
* @param array $items
*
* @return Client
*/
public function setMetadata(array $items): self {
$items = array_map('base64_encode', $items);
$this->metadata = $items;
return $this;
}
/**
* Get metadata.
*
* @return array
*/
public function getMetadata(): array {
return $this->metadata;
}
/**
* Add metadata.
*
* @param string $key
* @param string $value
*
* @return Client
*/
public function addMetadata(string $key, string $value): self {
$this->metadata[$key] = base64_encode($value);
return $this;
}
/**
* Get metadata for Upload-Metadata header.
*
* @return string
*/
protected function getUploadMetadataHeader(): string {
$metadata = [];
foreach ($this->getMetadata() as $key => $value) {
$metadata[] = "{$key} {$value}";
}
return implode(',', $metadata);
}
/**
* Set file properties.
*
* @param string $file File path.
* @param string|null $name File name.
*
* @return Client
*/
public function file(string $file, string $name = null): self {
$this->filePath = $file;
if (! file_exists($file) || ! is_readable($file)) {
throw new FileException('Cannot read file: ' . $file);
}
$this->fileName = $name ?? basename($this->filePath);
$this->fileSize = filesize($file);
$this->addMetadata('filename', $this->fileName);
return $this;
}
/**
* Handle client exception during patch request.
*
* @param ClientException $e
*
* @return \Exception
*/
protected function handleClientException(ClientException $e) {
$response = $e->getResponse();
$statusCode = $response !== null ? $response->getStatusCode() : HttpResponse::HTTP_INTERNAL_SERVER_ERROR;
if ($statusCode === HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
return new FileException('The uploaded file is corrupt.');
}
if ($statusCode === HttpResponse::HTTP_CONTINUE) {
return new ConnectionException('Connection aborted by user.');
}
if ($statusCode === HttpResponse::HTTP_UNSUPPORTED_MEDIA_TYPE) {
return new TusException('Unsupported media types.');
}
return new TusException((string) $response->getBody(), $statusCode);
}
/**
* Send HEAD request.
*
* @throws FileException
* @throws GuzzleException
*
* @return int
*/
protected function sendHeadRequest(): int {
if (!$this->getUrl()) {
throw new FileException('Upload URL not found.');
}
$response = $this->getClient()->head($this->getUrl());
$statusCode = $response->getStatusCode();
if ($statusCode !== HttpResponse::HTTP_OK) {
throw new FileException('File not found.');
}
return (int) current($response->getHeader('upload-offset'));
}
/**
* Send PATCH request.
*
* @param int $bytes
* @param int $offset
*
* @throws TusException
* @throws FileException
* @throws GuzzleException
* @throws ConnectionException
*
* @return int
*/
protected function sendPatchRequest(int $bytes, int $offset): int {
$data = $this->getData($offset, $bytes);
$headers = $this->headers + [
'Content-Type' => self::HEADER_CONTENT_TYPE,
'Content-Length' => (string)\strlen($data),
'Upload-Checksum' => $this->getUploadChecksumHeader(),
'Upload-Offset' => (string)$offset,
];
try {
$response = $this->getClient()->patch(
$this->getUrl(),
[
'body' => $data,
'headers' => $headers,
]
);
return (int) current($response->getHeader('upload-offset'));
} catch (ClientException $e) {
throw $this->handleClientException($e);
} catch (ConnectException $e) {
throw new ConnectionException("Couldn't connect to server.");
}
}
/**
* Check if file to read exists.
*
* @param string $filePath
* @param string $mode
*
* @throws FileException
*
* @return bool
*/
public function exists(string $filePath, string $mode = self::READ_BINARY): bool {
if ($filePath === self::INPUT_STREAM) {
return true;
}
if ($mode === self::READ_BINARY && ! file_exists($filePath)) {
throw new FileException('File not found.');
}
return true;
}
/**
* Get X bytes of data from file.
*
* @param int $offset
* @param int $bytes
*
* @return string
*/
protected function getData(int $offset, int $bytes): string {
$filePath = $this->getFilePath();
$mode = self::READ_BINARY;
$this->exists($filePath, $mode);
$handle = @fopen($filePath, $mode);
if ($handle === false) {
throw new FileException("Unable to open $filePath.");
}
$position = fseek($handle, $offset, SEEK_SET);
if ($position === -1) {
throw new FileException('Cannot move pointer to desired position.');
}
$data = fread($handle, $bytes);
if ($data === false) {
throw new FileException('Cannot read file.');
}
fclose($handle);
return $data;
}
/**
* Upload file.
*
* @param int $bytes Bytes to upload
*
* @throws TusException
* @throws GuzzleException
* @throws ConnectionException
*
* @return int
*/
public function upload(int $bytes = -1): int {
$bytes = $bytes < 0 ? $this->getFileSize() : $bytes;
$offset = $this->partialOffset < 0 ? 0 : $this->partialOffset;
try {
// Check if this upload exists with HEAD request.
$offset = $this->sendHeadRequest();
} catch (FileException | ClientException $e) {
// Create a new upload.
$this->url = $this->create();
} catch (ConnectException $e) {
throw new ConnectionException("Couldn't connect to server.");
}
// Now, resume upload with PATCH request.
return $this->sendPatchRequest($bytes, $offset);
}
/**
* Create resource with POST request.
*
* @throws FileException
* @throws GuzzleException
*
* @return string
*/
public function create(): string {
return $this->createWithUpload(0)['location'];
}
/**
* Create resource with POST request and upload data using the creation-with-upload extension.
*
* @see https://tus.io/protocols/resumable-upload.html#creation-with-upload
*
* @param int $bytes -1 => all data; 0 => no data
*
* @throws GuzzleException
*
* @return array [
* 'location' => string,
* 'offset' => int
* ]
*/
public function createWithUpload(int $bytes = -1): array {
$bytes = $bytes < 0 ? $this->fileSize : $bytes;
$headers = $this->headers + [
'Upload-Length' => (string)$this->fileSize,
'Upload-Checksum' => $this->getUploadChecksumHeader(),
'Upload-Metadata' => $this->getUploadMetadataHeader(),
];
$data = '';
if ($bytes > 0) {
$data = $this->getData(0, $bytes);
$headers += [
'Content-Type' => self::HEADER_CONTENT_TYPE,
'Content-Length' => (string)\strlen($data),
];
}
try {
$response = $this->getClient()->post(
$this->apiPath,
[
'body' => $data,
'headers' => $headers,
]
);
} catch (ClientException $e) {
$response = $e->getResponse();
}
$statusCode = $response->getStatusCode();
if ($statusCode !== HttpResponse::HTTP_CREATED) {
throw new FileException('Unable to create resource.');
}
$uploadOffset = $bytes > 0 ? current($response->getHeader('upload-offset')) : 0;
$uploadLocation = current($response->getHeader('location'));
return [
'location' => $uploadLocation,
'offset' => $uploadOffset,
];
}
}
/**
* --------------------------------
* Exceptions
* --------------------------------
*/
/**
* File exception class.
*/
class FileException extends \RuntimeException {
}
/**
* Connection exception class.
*/
class ConnectionException extends \Exception {
}
/**
* Tus exception class.
*/
class TusException extends \Exception {
}
+10 -11
View File
@@ -24,9 +24,6 @@ use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use TusPhp\Exception\ConnectionException;
use TusPhp\Exception\TusException;
use TusPhp\Tus\Client;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
@@ -34,6 +31,9 @@ use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
use TestHelpers\WaitHelper;
use TestHelpers\TUSClient;
use TestHelpers\TUSClient\TusException;
use TestHelpers\TUSClient\ConnectionException;
use Behat\Step\Given;
use Behat\Step\When;
@@ -339,7 +339,7 @@ class TUSContext implements Context {
$headers = \array_merge($headers, $checksumHeader);
}
$client = new Client(
$client = new TUSClient(
$this->featureContext->getBaseUrl(),
[
'verify' => false,
@@ -353,19 +353,18 @@ class TUSContext implements Context {
$suffixPath = $spaceId ?: $this->featureContext->getPersonalSpaceIdForUser($user);
}
$client->setChecksumAlgorithm('sha1');
$sourceFile = UploadHelper::getAcceptanceTestsDir() . $source;
$client->setApiPath(WebDavHelper::getDavPath($davPathVersion, $suffixPath));
$client->setMetadata($uploadMetadata);
$sourceFile = UploadHelper::getAcceptanceTestsDir() . $source;
$client->setKey((string)rand())->file($sourceFile, $destination);
$client->file($sourceFile, $destination);
$this->featureContext->pauseUploadDelete();
if ($bytes !== null) {
$client->file($sourceFile, $destination)->createWithUpload($client->getKey(), $bytes);
$client->createWithUpload($bytes);
} elseif (\filesize($sourceFile) === 0) {
$client->file($sourceFile, $destination)->createWithUpload($client->getKey(), 0);
$client->createWithUpload(0);
} elseif ($noOfChunks === 1) {
$client->file($sourceFile, $destination)->upload();
$client->upload();
} else {
$bytesPerChunk = (int)\ceil(\filesize($sourceFile) / $noOfChunks);
for ($i = 0; $i < $noOfChunks; $i++) {
@@ -399,7 +398,7 @@ class TUSContext implements Context {
);
$this->featureContext->setLastUploadDeleteTime(\time());
} catch (Exception $e) {
Assert::assertStringContainsString('TusPhp\Exception\FileException: Unable to create resource', (string)$e);
Assert::assertStringContainsString('FileException: Unable to create resource', (string)$e);
}
\unlink($temporaryFileName);
}
+11
View File
@@ -20,6 +20,9 @@
*/
use Composer\Autoload\ClassLoader;
use PHPUnit\TextUI\CliArguments\Builder as CliArgumentsBuilder;
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration;
$classLoader = new ClassLoader();
@@ -53,3 +56,11 @@ if (!\defined('ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER')) {
if (!\defined('TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER')) {
\define('TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER', ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER . '/server_tmp');
}
// With PHPUnit 10+, initialize the PHPUnit configuration registry with default values
// as config initialization never happens when using behat test runner.
// This is needed to be able to use some assertions like 'assertSame' from PHPUnit.
ConfigurationRegistry::init(
(new CliArgumentsBuilder())->fromParameters(['phpunit']),
DefaultConfiguration::create()
);
+9 -21
View File
@@ -1,27 +1,15 @@
{
"config": {
"platform": {
"php": "8.3"
},
"allow-plugins": {
"composer/package-versions-deprecated": true
}
},
"require": {
"behat/behat": "^3.24",
"behat/gherkin": "^4.9",
"behat/mink": "1.7.1",
"friends-of-behat/mink-extension": "^2.7",
"behat/behat": "^3.32",
"behat/gherkin": "^4.17",
"ciaranmcnulty/behat-stepthroughextension": "dev-master",
"rdx/behat-variables": "^1.2",
"sensiolabs/behat-page-object-extension": "^2.3",
"symfony/translation": "^5.4",
"sabre/xml": "^2.2",
"guzzlehttp/guzzle": "^7.7",
"phpunit/phpunit": "^9.6",
"laminas/laminas-ldap": "^2.15",
"ankitpokhrel/tus-php": "^2.4",
"swaggest/json-schema": "^0.12.42",
"splitbrain/php-archive": "^1.3"
"sabre/xml": "^4.1",
"guzzlehttp/guzzle": "^8.0",
"phpunit/phpunit": "^13.3",
"laminas/laminas-ldap": "^2.20",
"swaggest/json-schema": "^0.12",
"splitbrain/php-archive": "^1.5",
"symfony/http-foundation": "^8.1"
}
}
+7 -2
View File
@@ -1,5 +1,10 @@
{
"require": {
"owncloud/coding-standard": "^5.3"
"require-dev": {
"doctrine/coding-standard": "^14.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"require": {
"squizlabs/php_codesniffer": "^3.7"
}
}