Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+575
View File
@@ -0,0 +1,575 @@
# Acceptance Testing
To run tests in the test suite you have two options. You may go the easy way and just run the test suite in docker. But for some tasks you could also need to install the test suite natively, which requires a little more setup since PHP and some dependencies need to be installed.
Both ways to run tests with the test suites are described here.
## Table of Contents
- [Running Test Suite in Docker](#running-test-suite-in-docker)
- [Running API Tests](#running-api-tests)
- [Run Tests With Required Services](#run-tests-with-required-services)
- [Run Tests Only](#run-tests-only)
- [Skip Local Image Build While Running Tests](#skip-local-image-build-while-running-tests)
- [Check Test Logs](#check-test-logs)
- [Cleanup the Setup](#cleanup-the-setup)
- [Running WOPI Validator Tests](#running-wopi-validator-tests)
- [Running Test Suite in Local Environment](#running-test-suite-in-local-environment)
- [Running Tests With And Without `remote.php`](#running-tests-with-and-without-remotephp)
- [Running ENV Config Tests (@env-Config)](#running-env-config-tests-env-config)
- [Running Test Suite With Email Service (@email)](#running-test-suite-with-email-service-email)
- [Running Test Suite With Tika Service (@tikaServiceNeeded)](#running-test-suite-with-tika-service-tikaserviceneeded)
- [Running Test Suite With Antivirus Service (@antivirus)](#running-test-suite-with-antivirus-service-antivirus)
- [Running Test Suite With Federated Sharing (@ocm)](#running-test-suite-with-federated-sharing-ocm)
- [Running Text Preview Tests Containing Unicode Characters](#running-text-preview-tests-containing-unicode-characters)
- [Running All API Tests Locally](#running-all-api-tests-locally)
## Running Test Suite in Docker
Check the available commands and environment variables with:
```bash
make -C tests/acceptance/docker help
```
### Running API Tests
#### Run Tests With Required Services
We can run a single feature or a single test suite with different storage drivers.
1. Run a specific feature file:
```bash
BEHAT_FEATURE='tests/acceptance/features/apiGraphUserGroup/createUser.feature' \
make -C tests/acceptance/docker run-api-tests
```
or a single scenario in a feature:
```bash
BEHAT_FEATURE='tests/acceptance/features/apiGraphUserGroup/createUser.feature:24' \
make -C tests/acceptance/docker run-api-tests
```
2. Run a specific test suite:
```bash
BEHAT_SUITE='apiGraphUserGroup' \
make -C tests/acceptance/docker run-api-tests
```
3. Run with different storage driver (default is `posix`):
```bash
STORAGE_DRIVER='posix' \
BEHAT_SUITE='apiGraphUserGroup' \
make -C tests/acceptance/docker run-api-tests
```
4. Run the tests that require an email server (tests tagged with `@email`). Provide `START_EMAIL=true` while running the tests:
```bash
START_EMAIL=true \
BEHAT_FEATURE='tests/acceptance/features/apiNotification/emailNotification.feature' \
make -C tests/acceptance/docker run-api-tests
```
5. Run the tests that require tika service (tests tagged with `@tikaServiceNeeded`). Provide `START_TIKA=true` while running the tests:
```bash
START_TIKA=true \
BEHAT_FEATURE='tests/acceptance/features/apiSearchContent/contentSearch.feature' \
make -C tests/acceptance/docker run-api-tests
```
6. Run the tests that require an antivirus service (tests tagged with `@antivirus`). Provide `START_ANTIVIRUS=true` while running the tests:
```bash
START_ANTIVIRUS=true \
BEHAT_FEATURE='tests/acceptance/features/apiAntivirus/antivirus.feature' \
make -C tests/acceptance/docker run-api-tests
```
7. Run the wopi tests. Provide `ENABLE_WOPI=true` while running the tests:
```bash
ENABLE_WOPI=true \
BEHAT_FEATURE='tests/acceptance/features/apiCollaboration/checkFileInfo.feature' \
make -C tests/acceptance/docker run-api-tests
```
#### Run Tests Only
If you want to re-run the tests because of some failures or any other reason, you can use the following command to run only the tests without starting the services again.
Also, this command can be used to run the tests against the already hosted КуСфера server by providing the `TEST_SERVER_URL` and `USE_BEARER_TOKEN` environment variables.
> [!NOTE]
> You can utilize the following environment variables:
>
> - `BEHAT_FEATURE`
> - `BEHAT_SUITE`
> - `USE_BEARER_TOKEN`
> - `TEST_SERVER_URL`
```bash
BEHAT_FEATURE='tests/acceptance/features/apiGraphUserGroup/createUser.feature:24' \
make -C tests/acceptance/docker run-test-only
```
#### Skip Local Image Build While Running Tests
While running the tests, qsfera docker image is built with `qsfera/qsfera:dev` tag. If you want to skip building the local image, you can use `OC_IMAGE_TAG` env which must contain an available docker tag of the [qsfera/qsfera registry on Docker Hub](https://hub.docker.com/r/qsfera/qsfera) (e.g. 'latest').
```bash
OC_IMAGE_TAG=latest \
BEHAT_FEATURE='tests/acceptance/features/apiGraphUserGroup/createUser.feature' \
make -C tests/acceptance/docker run-api-tests
```
#### Check Test Logs
While a test is running or when it is finished, you can attach to the logs generated by the tests.
```bash
make -C tests/acceptance/docker show-test-logs
```
> [!NOTE]
> The log output is opened in `less`. You can navigate up and down with your cursors. By pressing "F" you can follow the latest line of the output.
#### Cleanup the Setup
Run the following command to clean all the resources created while running the tests:
```bash
make -C tests/acceptance/docker clean-all
```
### Running WOPI Validator Tests
#### Available Test Groups
```text
BaseWopiViewing
CheckFileInfoSchema
EditFlows
Locks
AccessTokens
GetLock
ExtendedLockLength
FileVersion
Features
PutRelativeFile
RenameFileIfCreateChildFileIsNotSupported
```
#### Run Test
```bash
TEST_GROUP=BaseWopiViewing docker compose -f tests/acceptance/docker/src/wopi-validator-test.yml up -d
```
#### Run Test (macOS)
Use the arm image for macOS to run the validator tests.
```bash
WOPI_VALIDATOR_IMAGE=scharfvi/wopi-validator \
TEST_GROUP=BaseWopiViewing \
docker compose -f tests/acceptance/docker/src/wopi-validator-test.yml up -d
```
## Running Test Suite in Local Environment
### Run КуСфера
Create an up-to-date КуСфера binary by [building КуСфера]({{< ref "build" >}})
To start КуСфера:
```bash
IDM_ADMIN_PASSWORD=admin \
qsfera/bin/qsfera init --insecure true
OC_INSECURE=true PROXY_ENABLE_BASIC_AUTH=true \
qsfera/bin/qsfera server
```
`PROXY_ENABLE_BASIC_AUTH` will allow the acceptance tests to make requests against the provisioning api (and other endpoints) using basic auth.
#### Run Local КуСфера Tests (prefix `api`) and Tests Transferred From Core (prefix `coreApi`)
```bash
make test-acceptance-api \
TEST_SERVER_URL=https://localhost:9200 \
```
Useful environment variables:
`TEST_SERVER_URL`: КуСфера server url. Please, adjust the server url according to your setup.
`BEHAT_FEATURE`: to run a single feature
Note:
A specific scenario from a feature can be run by adding `:<line-number>` at the end of the feature file path. For example, to run the scenario at line 26 of the feature file `apiGraphUserGroup/createUser.feature`, simply add the line number like this: `apiGraphUserGroup/createUser.feature:26`. Note that the line numbers mentioned in the examples might not always point to a scenario, so always check the line numbers before running the test.
> Example:
>
> BEHAT_FEATURE=tests/acceptance/features/apiGraphUserGroup/createUser.feature
>
> Or
>
> BEHAT_FEATURE=tests/acceptance/features/apiGraphUserGroup/createUser.feature:13
`BEHAT_SUITE`: to run a single suite
> Example:
>
> BEHAT_SUITE=apiGraph
`STORAGE_DRIVER`: to run tests with a different user storage driver. Available options are `decomposed` (default), `owncloudsql` and `decomposeds3`
> Example:
>
> STORAGE_DRIVER=owncloudsql
`STOP_ON_FAILURE`: to stop running tests after the first failure
> Example:
>
> STOP_ON_FAILURE=true
### Use Existing Tests for BDD
As a lot of scenarios are written for core, we can use those tests for Behaviour driven development in КуСфера.
Every scenario that does not work in КуСфера with `decomposed` storage, is listed in `tests/acceptance/expected-failures-decomposed-storage.md` with a link to the related issue.
Those scenarios are run in the ordinary acceptance test pipeline in CI. The scenarios that fail are checked against the
expected failures. If there are any differences then the CI pipeline fails.
If you want to work on a specific issue
1. locally run each of the tests marked with that issue in the expected failures file.
E.g.:
```bash
make test-acceptance-api \
TEST_SERVER_URL=https://localhost:9200 \
STORAGE_DRIVER=decomposed \
BEHAT_FEATURE='tests/acceptance/features/coreApiVersions/fileVersions.feature:141'
```
2. the tests will fail, try to understand how and why they are failing
3. fix the code
4. go back to 1. and repeat till the tests are passing.
5. remove those tests from the expected failures file
6. make a PR that has the fixed code, and the relevant lines removed from the expected failures file.
### Running Tests With And Without `remote.php`
By default, the tests are run with `remote.php` enabled. If you want to run the tests without `remote.php`, you can disable it by setting the environment variable `WITH_REMOTE_PHP=false` while running the tests.
```bash
WITH_REMOTE_PHP=false \
TEST_SERVER_URL="https://localhost:9200" \
make test-acceptance-api
```
### Running ENV Config Tests (@env-Config)
Test suites tagged with `@env-config` are used to test the environment variables that are used to configure КуСфера. These tests are special tests that require the КуСфера server to be run using [ocwrapper](https://github.com/qsfera/server/blob/main/tests/ocwrapper/README.md).
#### Run КуСфера With ocwrapper
```bash
# working dir: КуСфера repo root dir
# init КуСфера
IDM_ADMIN_PASSWORD=admin \
qsfera/bin/qsfera init --insecure true
# build the wrapper
cd tests/ocwrapper
make build
# run КуСфера
PROXY_ENABLE_BASIC_AUTH=true \
./bin/ocwrapper serve --bin=../../qsfera/bin/qsfera
```
#### Run the Tests
```bash
OC_WRAPPER_URL=http://localhost:5200 \
TEST_SERVER_URL="https://localhost:9200" \
BEHAT_FEATURE=tests/acceptance/features/apiAsyncUpload/delayPostprocessing.feature \
make test-acceptance-api
```
#### Writing New ENV Config Tests
While writing tests for a new КуСфера ENV configuration, please make sure to follow these guidelines:
1. Tag the test suite (or test scenarios) with `@env-config`
2. Use `OcConfigHelper.php` for helper functions - provides functions to reconfigure the running КуСфера instance.
3. Recommended: add the new step implementations in `OcConfigContext.php`
### Running Test Suite With Email Service (@email)
Test suites that are tagged with `@email` require an email service. We use inbucket as the email service in our tests.
#### Setup Inbucket
Run the following command to setup inbucket
```bash
docker run -d -p9000:9000 -p2500:2500 --name inbucket inbucket/inbucket
```
#### Run КуСфера
Documentation for environment variables is available [here](https://docs.qsfera.eu/services/notifications/#environment-variables)
```bash
# init КуСфера
IDM_ADMIN_PASSWORD=admin \
qsfera/bin/qsfera init --insecure true
# run КуСфера
PROXY_ENABLE_BASIC_AUTH=true \
OC_ADD_RUN_SERVICES=notifications \
NOTIFICATIONS_SMTP_HOST=localhost \
NOTIFICATIONS_SMTP_PORT=2500 \
NOTIFICATIONS_SMTP_INSECURE=true \
NOTIFICATIONS_SMTP_SENDER="КуСфера <noreply@example.com>" \
qsfera/bin/qsfera server
```
#### Run the Acceptance Test
Run the acceptance test with the following command:
```bash
TEST_SERVER_URL="https://localhost:9200" \
EMAIL_HOST="localhost" \
EMAIL_PORT=9000 \
BEHAT_FEATURE="tests/acceptance/features/apiNotification/emailNotification.feature" \
make test-acceptance-api
```
### Running Test Suite With Tika Service (@tikaServiceNeeded)
Test suites that are tagged with `@tikaServiceNeeded` require tika service.
#### Setup Tika Service
Run the following docker command to setup tika service
```bash
docker run -d -p 127.0.0.1:9998:9998 apache/tika
```
#### Run КуСфера
TODO: Documentation related to the content based search and tika extractor will be added later.
```bash
# init КуСфера
IDM_ADMIN_PASSWORD=admin \
qsfera/bin/qsfera init --insecure true
# run КуСфера
PROXY_ENABLE_BASIC_AUTH=true \
OC_INSECURE=true \
SEARCH_EXTRACTOR_TYPE=tika \
SEARCH_EXTRACTOR_TIKA_TIKA_URL=http://localhost:9998 \
SEARCH_EXTRACTOR_CS3SOURCE_INSECURE=true \
qsfera/bin/qsfera server
```
#### Run the Acceptance Test
Run the acceptance test with the following command:
```bash
TEST_SERVER_URL="https://localhost:9200" \
BEHAT_FEATURE="tests/acceptance/features/apiSearchContent/contentSearch.feature" \
make test-acceptance-api
```
### Running Test Suite With Antivirus Service (@antivirus)
Test suites that are tagged with `@antivirus` require antivirus service. TODO The available antivirus and the configuration related to them will be added latert. This documentation is only going to use `clamav` as antivirus.
#### Setup clamAV
**Option 1. Setup Locally**
Linux OS user:
Run the following command to set up calmAV and clamAV daemon
```bash
sudo apt install clamav clamav-daemon -y
```
Make sure that the clamAV daemon is up and running
```bash
sudo service clamav-daemon status
```
Note:
The commands are ubuntu specific and may differ according to your system. You can find information related to installation of clamAV in their official documentation [here](https://docs.clamav.net/manual/Installing/Packages.html).
Mac OS user:
Install ClamAV using [here](https://gist.github.com/mendozao/3ea393b91f23a813650baab9964425b9)
Start ClamAV daemon
```bash
/your/location/to/brew/Cellar/clamav/1.1.0/sbin/clamd
```
**Option 2. Setup clamAV With Docker**
Run `clamAV` through docker
```bash
docker run -d -p 3310:3310 opencloudeu/clamav-ci:latest
```
#### Run КуСфера
As `antivirus` service is not enabled by default we need to enable the service while running КуСфера server. We also need to enable `async upload` and as virus scan is performed in post-processing step, we need to set it as well. Documentation for environment variables related to antivirus is available [here](https://docs.qsfera.eu/services/antivirus/#environment-variables)
```bash
# init КуСфера
IDM_ADMIN_PASSWORD=admin \
qsfera/bin/qsfera init --insecure true
# run КуСфера
PROXY_ENABLE_BASIC_AUTH=true \
ANTIVIRUS_SCANNER_TYPE="clamav" \
ANTIVIRUS_CLAMAV_SOCKET="tcp://host.docker.internal:3310" \
POSTPROCESSING_STEPS="virusscan" \
OC_ASYNC_UPLOADS=true \
OC_ADD_RUN_SERVICES="antivirus" \
qsfera/bin/qsfera server
```
Note:
The value for `ANTIVIRUS_CLAMAV_SOCKET` is an example which needs adaption according your OS.
For antivirus running localy on Linux OS, use `ANTIVIRUS_CLAMAV_SOCKET= "/var/run/clamav/clamd.ctl"`.
For antivirus running localy on Mac OS, use `ANTIVIRUS_CLAMAV_SOCKET= "/tmp/clamd.sock"`.
For antivirus running with docker, use `ANTIVIRUS_CLAMAV_SOCKET= "tcp://host.docker.internal:3310"`
#### Run the Acceptance Test
Run the acceptance test with the following command:
```bash
TEST_SERVER_URL="https://localhost:9200" \
BEHAT_FEATURE="tests/acceptance/features/apiAntivirus/antivirus.feature" \
make test-acceptance-api
```
### Running Test Suite With Federated Sharing (@ocm)
Test suites that are tagged with `@ocm` require running two different КуСфера instances. TODO More detailed information and configuration related to it will be added later.
#### Setup First КуСфера Instance
```bash
# init КуСфера
IDM_ADMIN_PASSWORD=admin \
qsfera/bin/qsfera init --insecure true
# run КуСфера
OC_URL="https://localhost:9200" \
PROXY_ENABLE_BASIC_AUTH=true \
OC_ENABLE_OCM=true \
OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE="tests/config/local/providers.json" \
OC_ADD_RUN_SERVICES="ocm" \
OCM_OCM_INVITE_MANAGER_INSECURE=true \
OCM_OCM_SHARE_PROVIDER_INSECURE=true \
OCM_OCM_STORAGE_PROVIDER_INSECURE=true \
WEB_UI_CONFIG_FILE="tests/config/local/qsfera-web.json" \
qsfera/bin/qsfera server
```
The first КуСфера instance should be available at: https://localhost:9200/
#### Setup Second КуСфера Instance
You can run the second КуСфера instance in two ways:
**Option 1. Using `.vscode/launch.json`**
From the `Run and Debug` panel of VSCode, select `Fed КуСфера Server` and start the debugger.
**Option 2. Using env file**
```bash
# init КуСфера
source tests/config/local/.env-federation && qsfera/bin/qsfera init
# run КуСфера
qsfera/bin/qsfera server
```
The second КуСфера instance should be available at: https://localhost:10200/
Note:
To enable ocm in the web interface, you need to set the following envs:
`OC_ENABLE_OCM="true"`
`OC_ADD_RUN_SERVICES="ocm"`
#### Run the Acceptance Test
Run the acceptance test with the following command:
```bash
TEST_SERVER_URL="https://localhost:9200" \
TEST_SERVER_FED_URL="https://localhost:10200" \
BEHAT_FEATURE="tests/acceptance/features/apiOcm/ocm.feature" \
make test-acceptance-api
```
### Running Text Preview Tests Containing Unicode Characters
There are some tests that check the text preview of files containing Unicode characters. The КуСфера server by default cannot generate the thumbnail of such files correctly but it provides an environment variable to allow the use of custom fonts that support Unicode characters. So to run such tests successfully, we have to run the КуСфера server with this environment variable.
```bash
...
THUMBNAILS_TXT_FONTMAP_FILE="/path/to/fontsMap.json"
qsfera/bin/qsfera server
```
The sample `fontsMap.json` file is located in `tests/config/drone/fontsMap.json`.
```json
{
"defaultFont": "/path/to/qsfera/tests/config/drone/NotoSans.ttf"
}
```
## Running All API Tests Locally
### Build dev docker
```bash
make -C qsfera dev-docker
```
### Choose STORAGE_DRIVER
By default, the system uses `posix` storage. However, you can override this by setting the `STORAGE_DRIVER` environment variable.
### Run a script that starts the qsfera server in the docker and runs the API tests locally (for debugging purposes)
```bash
STORAGE_DRIVER=posix ./tests/acceptance/run_api_tests.sh
```
+2
View File
@@ -0,0 +1,2 @@
output
logs
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2018 Artur Neumann artur@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/>
*
*/
namespace TestHelpers\Asserts;
use PHPUnit\Framework\Assert;
use SimpleXMLElement;
/**
* WebDAV related asserts
*/
class WebDav extends Assert {
/**
*
* @param string|null $element exception|message|reason
* @param string|null $expectedValue
* @param array|null $responseXmlArray
* @param string|null $extraErrorText
*
* @return void
*/
public static function assertDavResponseElementIs(
?string $element,
?string $expectedValue,
?array $responseXmlArray,
?string $extraErrorText = ''
): void {
if ($extraErrorText !== '') {
$extraErrorText = $extraErrorText . " ";
}
self::assertArrayHasKey(
'value',
$responseXmlArray,
$extraErrorText . "responseXml does not have key 'value'"
);
if ($element === "exception") {
$result = $responseXmlArray['value'][0]['value'];
} elseif ($element === "message") {
$result = $responseXmlArray['value'][1]['value'];
} elseif ($element === "reason") {
$result = $responseXmlArray['value'][3]['value'];
} else {
self::fail(
__METHOD__ . " element must be one of exception, response or reason. But '$element' was passed in."
);
}
self::assertEquals(
$expectedValue,
$result,
__METHOD__ . " " . $extraErrorText . "Expected '$expectedValue' in element $element got '$result'"
);
}
/**
*
* @param SimpleXMLElement $responseXmlObject
* @param array|null $expectedShareTypes
*
* @return void
*/
public static function assertResponseContainsShareTypes(
SimpleXMLElement $responseXmlObject,
?array $expectedShareTypes
): void {
foreach ($expectedShareTypes as $row) {
$xmlPart = $responseXmlObject->xpath(
"//d:prop/oc:share-types/oc:share-type[.=" . $row[0] . "]"
);
self::assertNotEmpty(
$xmlPart,
"cannot find share-type '" . $row[0] . "'"
);
}
}
}
@@ -0,0 +1,106 @@
<?php declare(strict_types=1);
/**
* @author Niraj Acharya <niraj@jankaritech.com>
* @copyright Copyright (c) 2024 Niraj Acharya niraj@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/>
*
*/
namespace TestHelpers;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing Auth App API requests
*/
class AuthAppHelper {
/**
* @return string
*/
public static function getAuthAppEndpoint(): string {
return "/auth-app/tokens";
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
*
* @return ResponseInterface
*/
public static function listAllAppAuthTokensForUser(
string $baseUrl,
string $user,
string $password
): ResponseInterface {
$url = $baseUrl . self::getAuthAppEndpoint();
return HttpRequestHelper::sendRequest(
$url,
null,
"GET",
$user,
$password,
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param array $params
*
* @return ResponseInterface
*/
public static function createAppAuthToken(
string $baseUrl,
string $user,
string $password,
array $params,
): ResponseInterface {
$url = $baseUrl . self::getAuthAppEndpoint() . "?"
. http_build_query($params);
return HttpRequestHelper::sendRequest(
$url,
null,
"POST",
$user,
$password,
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $token
*
* @return ResponseInterface
*/
public static function deleteAppAuthToken(
string $baseUrl,
string $user,
string $password,
string $token
): ResponseInterface {
$url = $baseUrl . self::getAuthAppEndpoint() . "?token=$token";
return HttpRequestHelper::sendRequest(
$url,
null,
"DELETE",
$user,
$password,
);
}
}
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Context\Exception\ContextNotFoundException;
use Behat\Behat\Hook\Scope\ScenarioScope;
/**
* Helper for Behat environment configuration
*
*/
class BehatHelper {
/**
* @param ScenarioScope $scope
* @param InitializedContextEnvironment $environment
* @param string $class
*
* @return Context
*/
public static function getContext(
ScenarioScope $scope,
InitializedContextEnvironment $environment,
string $class
): Context {
try {
return $environment->getContext($class);
} catch (ContextNotFoundException $e) {
$context = new $class();
$environment->registerContext($context);
if (\method_exists($context, 'before')) {
$context->before($scope);
}
return $environment->getContext($class);
}
}
}
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2024 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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\OcConfigHelper;
/**
* A helper class for running КуСфера CLI commands
*/
class CliHelper {
/**
* @param array $body
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function runCommand(array $body): ResponseInterface {
$url = OcConfigHelper::getWrapperUrl() . "/command";
return OcConfigHelper::sendRequest($url, "POST", \json_encode($body));
}
}
@@ -0,0 +1,95 @@
<?php declare(strict_types=1);
/**
* @author Amrita Shrestha <amrita@jankaritech.com>
* @copyright Copyright (c) 2024 Amrita Shrestha amrita@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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing wopi requests
*/
class CollaborationHelper {
/**
* @param string $fileId
* @param string $app
* @param string $username
* @param string $password
* @param string $baseUrl
* @param string $xRequestId
* @param string|null $viewMode
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendPOSTRequestToAppOpen(
string $fileId,
string $app,
string $username,
string $password,
string $baseUrl,
string $xRequestId,
?string $viewMode = null,
): ResponseInterface {
$url = $baseUrl . "/app/open?app_name=$app&file_id=$fileId";
if ($viewMode) {
$url .= "&view_mode=$viewMode";
}
return HttpRequestHelper::post(
$url,
$xRequestId,
$username,
$password,
['Content-Type' => 'application/json']
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $parentContainerId
* @param string $file
* @param array|null $headers
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function createFile(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $parentContainerId,
string $file,
?array $headers = null
): ResponseInterface {
$url = $baseUrl . "/app/new?parent_container_id=$parentContainerId&filename=$file";
return HttpRequestHelper::post(
$url,
$xRequestId,
$user,
$password,
$headers
);
}
}
@@ -0,0 +1,178 @@
<?php declare(strict_types=1);
/**
* @author Prajwol Amatya <prajwol@jankaritech.com>
* @copyright Copyright (c) 2023 Prajwol Amatya prajwol@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/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing emails
*/
class EmailHelper {
/**
* @param string $emailAddress
*
* @return string
*/
public static function getMailBoxFromEmail(string $emailAddress): string {
return explode("@", $emailAddress)[0];
}
/**
* Returns the host and port where Email messages can be read and deleted
* by the test runner.
*
* @return string
*/
public static function getLocalEmailUrl(): string {
$localEmailHost = self::getLocalEmailHost();
$emailPort = \getenv('EMAIL_PORT');
if ($emailPort === false) {
$emailPort = "9000";
}
return "http://$localEmailHost:$emailPort";
}
/**
* Returns the host name or address of the Email server as seen from the
* point of view of the system-under-test.
*
* @return string
*/
public static function getEmailHost(): string {
$emailHost = \getenv('EMAIL_HOST');
if ($emailHost === false) {
$emailHost = "127.0.0.1";
}
return $emailHost;
}
/**
* Returns the host name or address of the Email server as seen from the
* point of view of the test runner.
*
* @return string
*/
public static function getLocalEmailHost(): string {
$localEmailHost = \getenv('LOCAL_EMAIL_HOST');
if ($localEmailHost === false) {
$localEmailHost = self::getEmailHost();
}
return $localEmailHost;
}
/**
* Returns general response information about the provided mailbox
* A mailbox is created automatically in InBucket for every unique email sender|receiver
*
* @param string $mailBox
* @param string|null $xRequestId
*
* @return array
* @throws GuzzleException
*/
public static function getMailBoxInformation(string $mailBox, ?string $xRequestId = null): array {
$response = HttpRequestHelper::get(
self::getLocalEmailUrl() . "/api/v1/mailbox/" . $mailBox,
$xRequestId,
null,
null,
['Content-Type' => 'application/json']
);
return \json_decode($response->getBody()->getContents());
}
/**
* returns body content of a specific email (mailBox) with email ID (mailbox Id)
*
* @param string $mailBox
* @param string $mailboxId
* @param string|null $xRequestId
*
* @return object
* @throws GuzzleException
*/
public static function getBodyOfAnEmailById(
string $mailBox,
string $mailboxId,
?string $xRequestId = null
): object {
$response = HttpRequestHelper::get(
self::getLocalEmailUrl() . "/api/v1/mailbox/" . $mailBox . "/" . $mailboxId,
$xRequestId,
null,
null,
['Content-Type' => 'application/json']
);
return \json_decode($response->getBody()->getContents());
}
/**
* Returns the body of the last received email for the provided receiver
*
* @param string $emailAddress
* @param string $xRequestId
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function getBodyOfLastEmail(
string $emailAddress,
string $xRequestId,
): string {
$mailBox = self::getMailBoxFromEmail($emailAddress);
$emails = self::getMailboxInformation($mailBox, $xRequestId);
if (!empty($emails)) {
$emailId = \array_pop($emails)->id;
$response = self::getBodyOfAnEmailById($mailBox, $emailId, $xRequestId);
$body = \str_replace(
"\r\n",
"\n",
\quoted_printable_decode($response->body->text . "\n" . $response->body->html)
);
return $body;
}
return "";
}
/**
* Deletes all the emails for the provided mailbox
*
* @param string $url
* @param string $mailBox
* @param string $xRequestId
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteAllEmails(
string $url,
string $mailBox,
string $xRequestId,
): ResponseInterface {
return HttpRequestHelper::delete(
$url . "/api/v1/mailbox/" . $mailBox,
$xRequestId,
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,133 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2023, ownCloud GmbH
*
* 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/>
*
*/
namespace TestHelpers;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Helper for logging HTTP requests and responses
*/
class HttpLogger {
/**
* @return string
*/
public static function getLogDir(): string {
return __DIR__ . '/../logs';
}
/**
* @return string
*/
public static function getFailedLogPath(): string {
return self::getLogDir() . "/failed.log";
}
/**
* @return string
*/
public static function getScenarioLogPath(): string {
return self::getLogDir() . "/scenario.log";
}
/**
* @param string $logFile
* @param string $logMessage
*
* @return void
*/
public static function writeLog(string $logFile, string $logMessage): void {
$file = \fopen($logFile, 'a+') or die('Cannot open file: ' . $logFile);
\fwrite($file, $logMessage);
\fclose($file);
}
/**
* @param RequestInterface $request
*
* @return void
*/
public static function logRequest(RequestInterface $request): void {
$method = $request->getMethod();
$path = $request->getUri()->getPath();
$query = $request->getUri()->getQuery();
$body = $request->getBody();
$headers = "";
foreach ($request->getHeaders() as $key => $value) {
$headers = $key . ": " . $value[0] . "\n";
}
$logMessage = "\t\t_______________________________________________________________________\n\n";
$logMessage .= "\t\t==> REQUEST [" . self::getCurrentDateTime() . "]\n";
$logMessage .= "\t\t$method $path\n";
$logMessage .= $query ? "\t\tQUERY: $query\n" : "";
$logMessage .= "\t\t$headers";
if ($body->getSize() > 0) {
$logMessage .= "\t\t==> REQ BODY\n";
$logMessage .= "\t\t$body\n";
}
$logMessage .= "\n";
self::writeLog(self::getScenarioLogPath(), $logMessage);
}
/**
* @param ResponseInterface $response
*
* @return void
*/
public static function logResponse(ResponseInterface $response): void {
$statusCode = $response->getStatusCode();
$statusMessage = $response->getReasonPhrase();
$body = $response->getBody();
$headers = "";
foreach ($response->getHeaders() as $key => $value) {
$headers = $key . ": " . $value[0] . "\n";
}
$logMessage = "\t\t<== RESPONSE [" . self::getCurrentDateTime() . "]\n";
$logMessage .= "\t\t$statusCode $statusMessage\n";
$logMessage .= "\t\t$headers";
if ($body->getSize() > 0) {
$logMessage .= "\t\t<== RES BODY\n";
foreach (\explode("\n", \strval($body)) as $line) {
$logMessage .= "\t\t$line\n";
}
}
// rewind the body stream so that later code can read from the start.
$response->getBody()->rewind();
$logMessage = \rtrim($logMessage) . "\n\n";
self::writeLog(self::getScenarioLogPath(), $logMessage);
}
/**
* Returns current date and time in format: 1999-01-31T23:01:59
*
* @return string
*/
public static function getCurrentDateTime(): string {
return date('Y-m-d\TG:i:s');
}
}
@@ -0,0 +1,766 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use SimpleXMLElement;
use Sabre\Xml\LibXMLException;
use Sabre\Xml\Reader;
use GuzzleHttp\Pool;
use Symfony\Component\HttpFoundation\Response;
/**
* Helper for HTTP requests
*/
class HttpRequestHelper {
public const HTTP_TOO_EARLY = 425;
public const HTTP_CONFLICT = 409;
/**
* Some systems-under-test do async post-processing of operations like upload,
* move, etc. If a client does a request on the resource before the post-processing
* is finished, then the server should return HTTP_TOO_EARLY "425". Clients are
* expected to retry the request "some time later" (tm).
*
* On such systems, when HTTP_TOO_EARLY status is received, the test code will
* retry the request at 1-second intervals until either some other HTTP status
* is received or the retry-limit is reached.
*
* @return int
*/
public static function numRetriesOnHttpTooEarly(): int {
// Currently reva and КуСфера may return HTTP_TOO_EARLY
// So try up to 10 times before giving up.
return STANDARD_RETRY_COUNT;
}
/**
*
* @param string $url
* @param string|null $xRequestId
* @param string|null $method
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequestOnce(
string $url,
?string $xRequestId = null,
?string $method = 'GET',
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
bool $stream = false,
?int $timeout = 0,
?Client $client = null,
): ResponseInterface {
$bearerToken = null;
if (TokenHelper::useBearerToken() && $user && $user !== 'public') {
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
// check token is still valid
$parsedUrl = parse_url($url);
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$baseUrl .= isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
$testUrl = $baseUrl . "/graph/v1.0/me";
if (OcHelper::isTestingOnReva()) {
$url = $baseUrl . "/ocs/v2.php/cloud/users/$user";
}
// check token validity with a GET request
$c = self::createClient(
$user,
$password,
$config,
$cookies,
$stream,
$timeout,
$bearerToken
);
$testReq = self::createRequest($testUrl, $xRequestId, 'GET');
try {
$testRes = $c->send($testReq);
} catch (RequestException $ex) {
$testRes = $ex->getResponse();
if ($testRes && $testRes->getStatusCode() === Response::HTTP_UNAUTHORIZED) {
// token is invalid or expired, get a new one
echo "[INFO] Bearer token expired or invalid, getting a new one...\n";
TokenHelper::clearAllTokens();
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
}
}
}
if ($client === null) {
$client = self::createClient(
$user,
$password,
$config,
$cookies,
$stream,
$timeout,
$bearerToken
);
}
if (WebdavHelper::isDAVRequest($url) && \str_starts_with($url, OcHelper::getServerUrl())) {
$urlHasRemotePhp = \str_contains($url, 'remote.php');
if (!WebDavHelper::withRemotePhp() && $urlHasRemotePhp) {
throw new Exception("remote.php is disabled but found in the URL: $url");
}
if (WebDavHelper::withRemotePhp() && !$urlHasRemotePhp) {
throw new Exception("remote.php is enabled but not found in the URL: $url");
}
if ($headers && \array_key_exists("Destination", $headers)) {
if (!WebDavHelper::withRemotePhp() && $urlHasRemotePhp) {
throw new Exception("remote.php is disabled but found in the URL: $url");
}
if (WebDavHelper::withRemotePhp() && !$urlHasRemotePhp) {
throw new Exception("remote.php is enabled but not found in the URL: $url");
}
}
}
$request = self::createRequest(
$url,
$xRequestId,
$method,
$headers,
$body
);
if ((\getenv('DEBUG_ACCEPTANCE_REQUESTS') !== false) || (\getenv('DEBUG_ACCEPTANCE_API_CALLS') !== false)) {
$debugRequests = true;
} else {
$debugRequests = false;
}
if ($debugRequests) {
self::debugRequest($request, $user, $password);
}
// The exceptions that might happen here include:
// ConnectException - in that case there is no response. Don't catch the exception.
// RequestException - if there is something in the response then pass it back.
// Otherwise, re-throw the exception.
// GuzzleException - something else unexpected happened. Don't catch the exception.
try {
$response = $client->send($request);
} catch (RequestException $ex) {
$response = $ex->getResponse();
//if the response was null for some reason do not return it but re-throw
if ($response === null) {
throw $ex;
}
}
HttpLogger::logResponse($response);
// wait for post-processing to finish if applicable
if (WebdavHelper::isDAVRequest($url)
&& \str_starts_with($url, OcHelper::getServerUrl())
&& \in_array($method, ["PUT", "MOVE", "COPY", "MKCOL"])
&& \in_array($response->getStatusCode(), [Response::HTTP_CREATED, Response::HTTP_NO_CONTENT])
&& OcConfigHelper::getPostProcessingDelay() === 0
) {
if (\in_array($method, ["MOVE", "COPY"])) {
$url = $headers['Destination'];
}
WebDavHelper::waitForPostProcessingToFinish(
$url,
$user,
$password,
$headers,
);
}
return $response;
}
/**
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $method
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
* @param bool|null $isGivenStep
*
* @return ResponseInterface
*
* @throws GuzzleException
*/
public static function sendRequest(
?string $url,
?string $xRequestId,
?string $method = 'GET',
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
bool $stream = false,
?int $timeout = 0,
?Client $client = null,
?bool $isGivenStep = false
): ResponseInterface {
if ((\getenv('DEBUG_ACCEPTANCE_RESPONSES') !== false) || (\getenv('DEBUG_ACCEPTANCE_API_CALLS') !== false)) {
$debugResponses = true;
} else {
$debugResponses = false;
}
$sendRetryLimit = self::numRetriesOnHttpTooEarly();
$sendCount = 0;
$sendExceptionHappened = false;
do {
$response = self::sendRequestOnce(
$url,
$xRequestId,
$method,
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream,
$timeout,
$client,
);
if ($response->getStatusCode() >= 400
&& $response->getStatusCode() !== self::HTTP_TOO_EARLY
&& $response->getStatusCode() !== self::HTTP_CONFLICT
) {
$sendExceptionHappened = true;
}
if ($debugResponses) {
self::debugResponse($response);
}
$sendCount = $sendCount + 1;
// Here we check if the response has status code 425 or is a 409 gotten from a Given step
// HTTP_TOO_EARLY (425) can happen if async processing of a previous request is still happening.
// For example, if a test uploads a file and then immediately tries to download it.
// HTTP_CONFLICT (409) can happen if the user has just been created in the previous step.
// The OCS API might not "realize" yet that the user exists. A folder creation (MKCOL) or maybe even
// a file upload might return 409.
// In all these cases we can try the API request again after a short time.
$loopAgain = !$sendExceptionHappened && ($response->getStatusCode() === self::HTTP_TOO_EARLY ||
($response->getStatusCode() === self::HTTP_CONFLICT && $isGivenStep)) &&
$sendCount <= $sendRetryLimit;
if ($loopAgain) {
// we need to repeat the send request, because we got HTTP_TOO_EARLY or HTTP_CONFLICT
// wait 1 second before sending again, to give the server some time
// to finish whatever post-processing it might be doing.
echo "[INFO] Received '" . $response->getStatusCode() .
"' status code, retrying request ($sendCount)...\n";
\sleep(1);
}
} while ($loopAgain);
return $response;
}
/**
* Print details about the request.
*
* @param RequestInterface|null $request
* @param string|null $user
* @param string|null $password
*
* @return void
*/
private static function debugRequest(?RequestInterface $request, ?string $user, ?string $password): void {
print("### AUTH: $user:$password\n");
print("### REQUEST: " . $request->getMethod() . " " . $request->getUri() . "\n");
self::printHeaders($request->getHeaders());
self::printBody($request->getBody());
print("\n### END REQUEST\n");
}
/**
* Print details about the response.
*
* @param ResponseInterface|null $response
*
* @return void
*/
private static function debugResponse(?ResponseInterface $response): void {
print("### RESPONSE\n");
print("Status: " . $response->getStatusCode() . "\n");
self::printHeaders($response->getHeaders());
self::printBody($response->getBody());
print("\n### END RESPONSE\n");
}
/**
* Print details about the headers.
*
* @param array|null $headers
*
* @return void
*/
private static function printHeaders(?array $headers): void {
if ($headers) {
print("Headers:\n");
foreach ($headers as $header => $value) {
if (\is_array($value)) {
print($header . ": " . \implode(', ', $value) . "\n");
} else {
print($header . ": " . $value . "\n");
}
}
} else {
print("Headers: none\n");
}
}
/**
* Print details about the body.
*
* @param StreamInterface|null $body
*
* @return void
*/
private static function printBody(?StreamInterface $body): void {
print("Body:\n");
\var_dump($body->getContents());
// Rewind the stream so that later code can read from the start.
$body->rewind();
}
/**
* Send the requests to the server in parallel.
* This function takes an array of requests and an optional client.
* It will send all the requests to the server using the Pool object in guzzle.
*
* @param array|null $requests
* @param Client|null $client
*
* @return array
*/
public static function sendBatchRequest(
?array $requests,
?Client $client
): array {
return Pool::batch($client, $requests);
}
/**
* Create a Guzzle Client
* This creates a client object that can be used later to send a request object(s)
*
* @param string|null $user
* @param string|null $password
* @param array|null $config
* @param CookieJar|null $cookies
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param string|null $bearerToken
*
* @return Client
*/
public static function createClient(
?string $user = null,
?string $password = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false,
?int $timeout = 0,
?string $bearerToken = null
): Client {
$options = [];
if ($bearerToken !== null) {
$options['headers']['Authorization'] = 'Bearer ' . $bearerToken;
} elseif ($user !== null) {
$options['auth'] = [$user, $password];
}
if ($config !== null) {
$options['config'] = $config;
}
if ($cookies !== null) {
$options['cookies'] = $cookies;
}
$options['stream'] = $stream;
$options['verify'] = false;
$options['timeout'] = $timeout ?: self::getRequestTimeout();
return new Client($options);
}
/**
* Create an HTTP request based on given parameters.
* This creates a RequestInterface object that can be used with a client to send a request.
* This enables us to create multiple requests in advance so that we can send them to the server at once in parallel.
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $method
* @param array|null $headers ['X-MyHeader' => 'value']
* @param string|array $body either the actual string to send in the body,
* or an array of key-value pairs to be converted
* into a body with http_build_query.
*
* @return RequestInterface
*/
public static function createRequest(
?string $url,
?string $xRequestId = '',
?string $method = 'GET',
?array $headers = null,
$body = null
): RequestInterface {
if ($headers === null) {
$headers = [];
}
if ($xRequestId !== '') {
$headers['X-Request-ID'] = $xRequestId;
}
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.
// But 'new Request' does not have the flexibility to do that.
// So we need to do it here.
$body = \http_build_query($body, '', '&');
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
$request = new Request(
$method,
$url,
$headers,
$body
);
HttpLogger::logRequest($request);
return $request;
}
/**
* same as HttpRequestHelper::sendRequest() but with "GET" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*/
public static function get(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'GET',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* same as HttpRequestHelper::sendRequest() but with "POST" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*/
public static function post(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'POST',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* same as HttpRequestHelper::sendRequest() but with "PUT" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*/
public static function put(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'PUT',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* same as HttpRequestHelper::sendRequest() but with "DELETE" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*
*/
public static function delete(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'DELETE',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* Parses the response as XML and returns a SimpleXMLElement with these
* registered namespaces:
* | prefix | namespace |
* | d | DAV: |
* | oc | http://owncloud.org/ns |
* | ocs | http://open-collaboration-services.org/ns |
*
* @param ResponseInterface $response
* @param string|null $exceptionText text to put at the front of exception messages
*
* @return SimpleXMLElement
* @throws Exception
*/
public static function getResponseXml(ResponseInterface $response, ?string $exceptionText = ''): SimpleXMLElement {
// rewind just to make sure we can reparse it in case it was parsed already...
$response->getBody()->rewind();
$contents = $response->getBody()->getContents();
try {
$responseXmlObject = new SimpleXMLElement($contents);
$responseXmlObject->registerXPathNamespace(
'ocs',
'http://open-collaboration-services.org/ns'
);
$responseXmlObject->registerXPathNamespace(
'oc',
'http://owncloud.org/ns'
);
$responseXmlObject->registerXPathNamespace(
'd',
'DAV:'
);
return $responseXmlObject;
} catch (Exception $e) {
if ($exceptionText !== '') {
$exceptionText = $exceptionText . ' ';
}
if ($contents === '') {
throw new Exception($exceptionText . "Received empty response where XML was expected");
}
$message = $exceptionText . "Exception parsing response body: \"" . $contents . "\"";
throw new Exception($message, 0, $e);
}
}
/**
* parses the body content of $response and returns an array representing the XML
* This function returns an array with the following three elements:
* * name - The root element name.
* * value - The value for the root element.
* * attributes - An array of attributes.
*
* @param ResponseInterface $response
*
* @return array
*/
public static function parseResponseAsXml(ResponseInterface $response): array {
// rewind so that we can reparse it if it was parsed already
$response->getBody()->rewind();
$body = $response->getBody()->getContents();
$parsedResponse = [];
if ($body && \substr($body, 0, 1) === '<') {
try {
$reader = new Reader();
$reader->xml($body);
$parsedResponse = $reader->parse();
} catch (LibXMLException $e) {
// Sometimes the body can be a real page of HTML and text.
// So it may not be a complete ordinary piece of XML.
// The XML parse might fail with an exception message like:
// Opening and ending tag mismatch: link line 31 and head.
}
}
return $parsedResponse;
}
/**
* @return int
*/
public static function getRequestTimeout(): int {
$timeout = \getenv("REQUEST_TIMEOUT");
return (int)$timeout ?: HTTP_REQUEST_TIMEOUT;
}
/**
* returns json decoded body content of a json response as an object
*
* @param ResponseInterface $response
*
* @return mixed
*/
public static function getJsonDecodedResponseBodyContent(ResponseInterface $response): mixed {
return json_decode($response->getBody()->getContents(), null, 512, JSON_THROW_ON_ERROR);
}
/**
* @return bool
*/
public static function sendScenarioLineReferencesInXRequestId(): bool {
return (\getenv("SEND_SCENARIO_LINE_REFERENCES") === "true");
}
/**
* @return string
*/
public static function getXRequestIdRegex(): string {
if (self::sendScenarioLineReferencesInXRequestId()) {
return '/^[a-zA-Z]+\/[a-zA-Z]+\.feature:\d+(-\d+)?$/';
}
$host = gethostname();
return "/^$host\/.*$/";
}
}
@@ -0,0 +1,141 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2023 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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\ConnectException;
use TestHelpers\HttpRequestHelper;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for configuring КуСфера server
*/
class OcConfigHelper {
public static $postProcessingDelay = 0;
/**
* @return int
*/
public static function getPostProcessingDelay(): int {
return self::$postProcessingDelay;
}
/**
* @param string $postProcessingDelay
*
* @return void
*/
public static function setPostProcessingDelay(string $postProcessingDelay): void {
// extract number from string
$delay = (int) filter_var($postProcessingDelay, FILTER_SANITIZE_NUMBER_INT);
self::$postProcessingDelay = $delay;
}
/**
* @param string $url
* @param string $method
* @param ?string $body
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequest(
string $url,
string $method,
?string $body = ""
): ResponseInterface {
$client = HttpRequestHelper::createClient();
$request = new Request(
$method,
$url,
[],
$body
);
try {
$response = $client->send($request);
} catch (ConnectException $e) {
throw new \Error(
"Cannot connect to the ocwrapper at the moment,"
. "make sure that ocwrapper is running before proceeding with the test run.\n"
. $e->getMessage()
);
} catch (GuzzleException $ex) {
$response = $ex->getResponse();
if ($response === null) {
throw $ex;
}
}
return $response;
}
/**
* @return string
*/
public static function getWrapperUrl(): string {
$url = \getenv("OC_WRAPPER_URL");
if ($url === false) {
$url = "http://localhost:5200";
}
return $url;
}
/**
* @param array $envs
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function reConfigureOc(array $envs): ResponseInterface {
$url = self::getWrapperUrl() . "/config";
return self::sendRequest($url, "PUT", \json_encode($envs));
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function rollbackOc(): ResponseInterface {
$url = self::getWrapperUrl() . "/rollback";
return self::sendRequest($url, "DELETE");
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function stopQsfera(): ResponseInterface {
$url = self::getWrapperUrl() . "/stop";
return self::sendRequest($url, "POST");
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function startQsfera(): ResponseInterface {
$url = self::getWrapperUrl() . "/start";
return self::sendRequest($url, "POST");
}
}
@@ -0,0 +1,323 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2020 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
/**
* Class StorageDriver
*
* @package TestHelpers
*/
abstract class StorageDriver {
public const DECOMPOSED = "DECOMPOSED";
public const EOS = "EOS";
public const OWNCLOUD = "OWNCLOUD";
public const DECOMPOSEDS3 = "DECOMPOSEDS3";
public const POSIX = "POSIX";
}
/**
* Class OcHelper
*
* Helper functions that are needed to run tests on КуСфера server
*
* @package TestHelpers
*/
class OcHelper {
public const STORAGE_DRIVERS = [
StorageDriver::DECOMPOSED,
StorageDriver::EOS,
StorageDriver::OWNCLOUD,
StorageDriver::DECOMPOSEDS3,
StorageDriver::POSIX
];
/**
* @return string
*/
public static function getServerUrl(): string {
if (\getenv('TEST_SERVER_URL')) {
return \getenv('TEST_SERVER_URL');
}
return 'https://localhost:9200';
}
/**
* @return string
*/
public static function getFederatedServerUrl(): string {
if (\getenv('TEST_SERVER_FED_URL')) {
return \getenv('TEST_SERVER_FED_URL');
}
return 'https://localhost:10200';
}
/**
* @return string
*/
public static function getCollaborationServiceUrl(): string {
if (\getenv("COLLABORATION_SERVICE_URL")) {
return \getenv("COLLABORATION_SERVICE_URL");
}
return "http://localhost:9300";
}
/**
* @return bool
*/
public static function isTestingOnReva(): bool {
return (\getenv("TEST_REVA") === "true");
}
/**
* @return bool
*/
public static function isUsingPreparedLdapUsers(): bool {
return (\getenv("USE_PREPARED_LDAP_USERS") === "true");
}
/**
* @return bool|string false if no command given or the command as string
*/
public static function getDeleteUserDataCommand() {
$cmd = \getenv("DELETE_USER_DATA_CMD");
if ($cmd === false || \trim($cmd) === "") {
return false;
}
return $cmd;
}
/**
* @return string
* @throws Exception
*/
public static function getStorageDriver(): string {
$storageDriver = (\getenv("STORAGE_DRIVER"));
if ($storageDriver === false) {
return StorageDriver::DECOMPOSED;
}
$storageDriver = \strtoupper($storageDriver);
if (!\in_array($storageDriver, self::STORAGE_DRIVERS)) {
throw new Exception(
"Invalid storage driver. " .
"STORAGE_DRIVER must be '" . \join(", ", self::STORAGE_DRIVERS) . "'"
);
}
return $storageDriver;
}
/**
* @param array $users
*
* @return void
* @throws Exception
*/
public static function deleteRevaUserData(?array $users = []): void {
$deleteCmd = self::getDeleteUserDataCommand();
if (self::getStorageDriver() === StorageDriver::POSIX) {
\exec($deleteCmd);
return;
}
foreach ($users as $user) {
if (\is_array($user)) {
$user = $user["actualUsername"];
}
if ($deleteCmd === false) {
if (self::getStorageDriver() === StorageDriver::OWNCLOUD) {
self::recurseRmdir(self::getOcRevaDataRoot() . $user);
}
continue;
} elseif (self::getStorageDriver() === StorageDriver::EOS) {
$deleteCmd = \str_replace(
"%s",
$user[0] . '/' . $user,
$deleteCmd
);
} else {
$deleteCmd = \sprintf($deleteCmd, $user);
}
\exec($deleteCmd);
}
}
/**
* Helper for Recursive Copy of file/folder
* For more info check this out https://gist.github.com/gserrano/4c9648ec9eb293b9377b
*
* @param string|null $source
* @param string|null $destination
*
* @return void
*/
public static function recurseCopy(?string $source, ?string $destination): void {
$dir = \opendir($source);
@\mkdir($destination);
while (($file = \readdir($dir)) !== false) {
if (($file != '.') && ($file != '..')) {
if (\is_dir($source . '/' . $file)) {
self::recurseCopy($source . '/' . $file, $destination . '/' . $file);
} else {
\copy($source . '/' . $file, $destination . '/' . $file);
}
}
}
\closedir($dir);
}
/**
* @return int
*/
public static function getLdapPort(): int {
$port = \getenv("REVA_LDAP_PORT");
return $port ? (int)$port : 636;
}
/**
* @return bool
*/
public static function useSsl(): bool {
$useSsl = \getenv("REVA_LDAP_USESSL");
if ($useSsl === false) {
return (self::getLdapPort() === 636);
} else {
return $useSsl === "true";
}
}
/**
* @return string
*/
public static function getBaseDN(): string {
$dn = \getenv("REVA_LDAP_BASE_DN");
return $dn ?: "dc=example,dc=org";
}
/**
* @return string
*/
public static function getGroupsOU(): string {
$ou = \getenv("REVA_LDAP_GROUPS_OU");
return $ou ?: "TestGroups";
}
/**
* @return string
*/
public static function getUsersOU(): string {
$ou = \getenv("REVA_LDAP_USERS_OU");
return $ou ?: "TestUsers";
}
/**
* @return string
*/
public static function getGroupSchema(): string {
$schema = \getenv("REVA_LDAP_GROUP_SCHEMA");
return $schema ?: "rfc2307";
}
/**
* @return string
*/
public static function getHostname(): string {
$hostname = \getenv("REVA_LDAP_HOSTNAME");
return $hostname ?: "localhost";
}
/**
* @return string
*/
public static function getBindDN(): string {
$dn = \getenv("REVA_LDAP_BIND_DN");
return $dn ?: "cn=admin,dc=example,dc=org";
}
/**
* @return string
*/
public static function getBindPassword(): string {
$pw = \getenv("REVA_LDAP_BIND_PASSWORD");
return $pw ?: "";
}
/**
* @return string
*/
private static function getOcRevaDataRoot(): string {
$root = \getenv("OC_REVA_DATA_ROOT");
if ($root === false || $root === "") {
$root = "/var/tmp/qsfera/qsfera/";
}
if (!\file_exists($root)) {
echo "WARNING: reva data root folder ($root) does not exist\n";
}
return $root;
}
/**
* @param string|null $dir
*
* @return bool
*/
private static function recurseRmdir(?string $dir): bool {
if (\file_exists($dir) === true) {
$files = \array_diff(\scandir($dir), ['.', '..']);
foreach ($files as $file) {
if (\is_dir("$dir/$file")) {
self::recurseRmdir("$dir/$file");
} else {
\unlink("$dir/$file");
}
}
return \rmdir($dir);
}
return true;
}
/**
* On Eos storage backend when the user data is cleared after test run
* Running another test immediately fails. So Send this request to create user home directory
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $xRequestId
*
* @return void
* @throws GuzzleException
*/
public static function createEOSStorageHome(
?string $baseUrl,
?string $user,
?string $password,
?string $xRequestId = ''
): void {
HttpRequestHelper::get(
$baseUrl . "/ocs/v2.php/apps/notifications/api/v1/notifications",
$xRequestId,
$user,
$password
);
}
}
@@ -0,0 +1,205 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <scharf.vi@gmail.com>
* @copyright Copyright (c) 2024 Viktor Scharf <scharf.vi@gmail.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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing federation server requests
*/
class OcmHelper {
/**
* @return string[]
*/
private static function getRequestHeaders(): array {
return [
'Content-Type' => 'application/json',
];
}
/**
* @param string $baseUrl
* @param string $path
*
* @return string
*/
public static function getFullUrl(string $baseUrl, string $path): string {
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= 'sciencemesh/' . $path;
return $fullUrl;
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string|null $email
* @param string|null $description
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function createInvitation(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
?string $email = null,
?string $description = null
): ResponseInterface {
$body = [
"description" => $description,
"recipient" => $email
];
$url = self::getFullUrl($baseUrl, 'generate-invite');
return HttpRequestHelper::post(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders(),
\json_encode($body)
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $token
* @param string $providerDomain
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function acceptInvitation(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $token,
string $providerDomain
): ResponseInterface {
$body = [
"token" => $token,
"providerDomain" => $providerDomain
];
$url = self::getFullUrl($baseUrl, 'accept-invite');
return HttpRequestHelper::post(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders(),
\json_encode($body)
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function findAcceptedUsers(
string $baseUrl,
string $xRequestId,
string $user,
string $password
): ResponseInterface {
$url = self::getFullUrl($baseUrl, 'find-accepted-users');
return HttpRequestHelper::get(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders()
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function listInvite(
string $baseUrl,
string $xRequestId,
string $user,
string $password
): ResponseInterface {
$url = self::getFullUrl($baseUrl, 'list-invite');
return HttpRequestHelper::get(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders()
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $userId
* @param string $idp
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteConnection(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $userId,
string $idp
): ResponseInterface {
$url = self::getFullUrl($baseUrl, 'delete-accepted-user');
$body = [
"idp" => $idp,
"user_id" => $userId
];
return HttpRequestHelper::delete(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders(),
\json_encode($body)
);
}
}
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Helper to make requests to the OCS API
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class OcsApiHelper {
/**
* @param string|null $baseUrl
* @param string|null $user if set to null no authentication header will be sent
* @param string|null $password
* @param string|null $method HTTP Method
* @param string|null $path
* @param string|null $xRequestId
* @param mixed $body array of key, value pairs e.g ['value' => 'yes']
* @param int|null $ocsApiVersion (1|2) default 2
* @param array|null $headers
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequest(
?string $baseUrl,
?string $user,
?string $password,
?string $method,
?string $path,
?string $xRequestId = '',
$body = [],
?int $ocsApiVersion = 2,
?array $headers = []
): ResponseInterface {
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= "ocs/v$ocsApiVersion.php" . $path;
$headers['OCS-APIREQUEST'] = true;
return HttpRequestHelper::sendRequest($fullUrl, $xRequestId, $method, $user, $password, $headers, $body);
}
/**
*
* @param string|null $baseUrl
* @param string|null $method HTTP Method
* @param string|null $path
* @param string|null $xRequestId
* @param mixed $body array of key, value pairs e.g ['value' => 'yes']
* @param int|null $ocsApiVersion (1|2) default 2
* @param array|null $headers
*
* @return RequestInterface
*/
public static function createOcsRequest(
?string $baseUrl,
?string $method,
?string $path,
?string $xRequestId = '',
$body = [],
?int $ocsApiVersion = 2,
?array $headers = []
): RequestInterface {
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= "ocs/v$ocsApiVersion.php" . $path;
return HttpRequestHelper::createRequest(
$fullUrl,
$xRequestId,
$method,
$headers,
$body
);
}
}
@@ -0,0 +1,335 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2024 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/>
*
*/
namespace TestHelpers;
use TestHelpers\HttpRequestHelper;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
/**
* A helper class for КуСфера settings
*/
class SettingsHelper {
private static string $settingsEndpoint = '/api/v0/settings/';
/**
* @param string $baseUrl
* @param string $path
*
* @return string
*/
public static function buildFullUrl(string $baseUrl, string $path): string {
return $baseUrl . self::$settingsEndpoint . $path;
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getBundlesList(
string $baseUrl,
string $user,
string $password,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "bundles-list");
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
"{}"
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $bundleName
* @param string $xRequestId
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*/
public static function getBundleByName(
string $baseUrl,
string $user,
string $password,
string $bundleName,
string $xRequestId
): array {
$response = self::getBundlesList($baseUrl, $user, $password, $xRequestId);
Assert::assertEquals(201, $response->getStatusCode(), "Failed to get bundles list");
$bundlesList = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
foreach ($bundlesList->bundles as $value) {
if ($value->displayName === $bundleName) {
return json_decode(json_encode($value), true);
}
}
return [];
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getRolesList(
string $baseUrl,
string $user,
string $password,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "roles-list");
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
"{}"
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $assigneeId
* @param string $roleId
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function assignRoleToUser(
string $baseUrl,
string $user,
string $password,
string $assigneeId,
string $roleId,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "assignments-add");
$body = json_encode(["account_uuid" => $assigneeId, "role_id" => $roleId], JSON_THROW_ON_ERROR);
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $userId
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getAssignmentsList(
string $baseUrl,
string $user,
string $password,
string $userId,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "assignments-list");
$body = json_encode(["account_uuid" => $userId], JSON_THROW_ON_ERROR);
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getValuesList(
string $baseUrl,
string $user,
string $password,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "values-list");
$body = json_encode(["account_uuid" => "me"], JSON_THROW_ON_ERROR);
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return bool
*
* @throws GuzzleException
* @throws Exception
*/
public static function getAutoAcceptSharesSettingValue(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): bool {
$response = self::getValuesList($baseUrl, $user, $password, $xRequestId);
Assert::assertEquals(201, $response->getStatusCode(), "Failed to get values list");
$valuesList = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
if (empty($valuesList)) {
return true;
}
foreach ($valuesList->values as $value) {
if ($value->identifier->setting === "auto-accept-shares") {
return $value->value->boolValue;
}
}
return true;
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
*
* @throws GuzzleException
* @throws Exception
*/
public static function getLanguageSettingValue(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): string {
$response = self::getValuesList($baseUrl, $user, $password, $xRequestId);
Assert::assertEquals(201, $response->getStatusCode(), "Failed to get values list");
$valuesList = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
// if no language is set, the request body is empty return English as the default language
if (empty($valuesList)) {
return "en";
}
foreach ($valuesList->values as $value) {
if ($value->identifier->setting === "language") {
return $value->value->listValue->values[0]->stringValue;
}
}
// if a language setting was still not found, return English
return "en";
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $body
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function updateSettings(
string $baseUrl,
string $user,
string $password,
string $body,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "values-save");
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
}
@@ -0,0 +1,406 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Behat\Testwork\Hook\Scope\HookScope;
use GuzzleHttp\Exception\GuzzleException;
use Exception;
use SimpleXMLElement;
/**
* Helper to set up UI / Integration tests
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class SetupHelper extends \PHPUnit\Framework\Assert {
private static ?string $baseUrl = null;
private static ?string $adminUsername = null;
private static ?string $adminPassword = null;
/**
*
* @param HookScope $scope
*
* @return array of suite context parameters
*/
public static function getSuiteParameters(HookScope $scope): array {
return $scope->getEnvironment()->getSuite()
->getSettings() ['context'] ['parameters'];
}
/**
*
* @param string|null $adminUsername
* @param string|null $adminPassword
* @param string|null $baseUrl
*
* @return void
*/
public static function init(
?string $adminUsername,
?string $adminPassword,
?string $baseUrl
): void {
foreach (\func_get_args() as $variableToCheck) {
if (!\is_string($variableToCheck)) {
throw new \InvalidArgumentException(
"mandatory argument missing or wrong type ($variableToCheck => "
. \gettype($variableToCheck) . ")"
);
}
}
self::$adminUsername = $adminUsername;
self::$adminPassword = $adminPassword;
self::$baseUrl = \rtrim($baseUrl, '/');
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
* @param string|null $xRequestId
*
* @return SimpleXMLElement
* @throws GuzzleException
* @throws Exception
*/
public static function getSysInfo(
?string $baseUrl,
?string $adminUsername,
?string $adminPassword,
?string $xRequestId = ''
): SimpleXMLElement {
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"GET",
"/apps/testing/api/v1/sysinfo",
$xRequestId
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not get sysinfo " . $result->getReasonPhrase()
);
}
return HttpRequestHelper::getResponseXml($result, __METHOD__)->data;
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
* @param string|null $xRequestId
*
* @return string
* @throws GuzzleException
*/
public static function getServerRoot(
?string $baseUrl,
?string $adminUsername,
?string $adminPassword,
?string $xRequestId = ''
): string {
$sysInfo = self::getSysInfo(
$baseUrl,
$adminUsername,
$adminPassword,
$xRequestId
);
// server_root is a SimpleXMLElement object that "wraps" a string.
/// We want the bare string.
return (string) $sysInfo->server_root;
}
/**
* @param string|null $adminUsername
* @param string|null $callerName
*
* @return string
* @throws Exception
*/
private static function checkAdminUsername(?string $adminUsername, ?string $callerName): ?string {
if (self::$adminUsername === null
&& $adminUsername === null
) {
throw new Exception(
"$callerName called without adminUsername - pass the username or call SetupHelper::init"
);
}
if ($adminUsername === null) {
$adminUsername = self::$adminUsername;
}
return $adminUsername;
}
/**
* @param string|null $adminPassword
* @param string|null $callerName
*
* @return string
* @throws Exception
*/
private static function checkAdminPassword(?string $adminPassword, ?string $callerName): string {
if (self::$adminPassword === null
&& $adminPassword === null
) {
throw new Exception(
"$callerName called without adminPassword - pass the password or call SetupHelper::init"
);
}
if ($adminPassword === null) {
$adminPassword = self::$adminPassword;
}
return $adminPassword;
}
/**
* @param string|null $baseUrl
* @param string|null $callerName
*
* @return string
* @throws Exception
*/
private static function checkBaseUrl(?string $baseUrl, ?string $callerName): ?string {
if (self::$baseUrl === null
&& $baseUrl === null
) {
throw new Exception(
"$callerName called without baseUrl - pass the baseUrl or call SetupHelper::init"
);
}
if ($baseUrl === null) {
$baseUrl = self::$baseUrl;
}
return $baseUrl;
}
/**
*
* @param string|null $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function mkDirOnServer(
?string $dirPathFromServerRoot,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "mkDirOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "mkDirOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "mkDirOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"POST",
"/apps/testing/api/v1/dir",
$xRequestId,
['dir' => $dirPathFromServerRoot]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not create directory $dirPathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
*
* @param string|null $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function rmDirOnServer(
?string $dirPathFromServerRoot,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "rmDirOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "rmDirOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "rmDirOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"DELETE",
"/apps/testing/api/v1/dir",
$xRequestId,
['dir' => $dirPathFromServerRoot]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not delete directory $dirPathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
*
* @param string|null $filePathFromServerRoot e.g. 'app2/myapp/appinfo/info.xml'
* @param string|null $content
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function createFileOnServer(
?string $filePathFromServerRoot,
?string $content,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "createFileOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "createFileOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "createFileOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"POST",
"/apps/testing/api/v1/file",
$xRequestId,
[
'file' => $filePathFromServerRoot,
'content' => $content
]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not create file $filePathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
*
* @param string|null $filePathFromServerRoot e.g. 'app2/myapp/appinfo/info.xml'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function deleteFileOnServer(
?string $filePathFromServerRoot,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "deleteFileOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "deleteFileOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "deleteFileOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"DELETE",
"/apps/testing/api/v1/file",
$xRequestId,
[
'file' => $filePathFromServerRoot
]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not delete file $filePathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
* @param string|null $fileInCore e.g. 'app2/myapp/appinfo/info.xml'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function readFileFromServer(
?string $fileInCore,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): string {
$baseUrl = self::checkBaseUrl($baseUrl, "readFile");
$adminUsername = self::checkAdminUsername(
$adminUsername,
"readFile"
);
$adminPassword = self::checkAdminPassword(
$adminPassword,
"readFile"
);
$response = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
'GET',
"/apps/testing/api/v1/file?file=$fileInCore",
$xRequestId
);
self::assertSame(
200,
$response->getStatusCode(),
"Failed to read the file $fileInCore"
);
$localContent = HttpRequestHelper::getResponseXml($response, __METHOD__);
$localContent = (string)$localContent->data->element->contentUrlEncoded;
return \urldecode($localContent);
}
}
@@ -0,0 +1,268 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use SimpleXMLElement;
/**
* manage Shares via OCS API
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class SharingHelper {
public const PERMISSION_TYPES = [
'read' => 1,
'update' => 2,
'create' => 4,
'delete' => 8,
'invite' => 0
];
public const SHARE_TYPES = [
'user' => 0,
'group' => 1,
'public_link' => 3,
'federated' => 6,
];
public const SHARE_STATES = [
'accepted' => 0,
'pending' => 1,
'rejected' => 2,
'declined' => 2, // declined is a synonym for rejected.
];
/**
*
* @param string $baseUrl baseURL of the КуСфера installation without /ocs.
* @param string $user user that creates the share.
* @param string $password password of the user that creates the share.
* @param string $path The path to the file or folder which should be shared.
* @param string|int $shareType The type of the share. This can be one of:
* 0 = user, 1 = group, 3 = public (link),
* 6 = federated (cloud share).
* Pass either the number or the keyword.
* @param string $xRequestId
* @param string|null $shareWith The user or group id with which the file should
* be shared.
* @param boolean|null $publicUpload Whether to allow public upload to a public
* shared folder.
* @param string|null $sharePassword The password to protect the public link
* share with.
* @param string|int|string[]|int[]|null $permissions The permissions to set on the share.
* 1 = read; 2 = update; 4 = create;
* 8 = delete; 16 = share
* (default: 31, for public shares: 1)
* Pass either the (total) number or array of numbers,
* or any of the above keywords or array of keywords.
* @param string|null $linkName A (human-readable) name for the share,
* which can be up to 64 characters in length.
* @param string|null $expireDate An expire date for public link shares.
* This argument expects a date string
* in the format 'YYYY-MM-DD'.
* @param string|null $space_ref
* @param int $ocsApiVersion
* @param int $sharingApiVersion
* @param string $sharingApp
*
* @return ResponseInterface
* @throws InvalidArgumentException|GuzzleException
*/
public static function createShare(
string $baseUrl,
string $user,
string $password,
string $path,
$shareType,
string $xRequestId = '',
?string $shareWith = null,
?bool $publicUpload = false,
?string $sharePassword = null,
$permissions = null,
?string $linkName = null,
?string $expireDate = null,
?string $space_ref = null,
int $ocsApiVersion = 1,
int $sharingApiVersion = 1,
string $sharingApp = 'files_sharing'
): ResponseInterface {
$fd = [];
foreach ([$path, $baseUrl, $user, $password] as $variableToCheck) {
if (!\is_string($variableToCheck)) {
throw new InvalidArgumentException(
"mandatory argument missing or wrong type ($variableToCheck => "
. \gettype($variableToCheck) . ")"
);
}
}
if ($permissions !== null) {
$fd['permissions'] = self::getPermissionSum($permissions);
} elseif ($shareType !== "public_link") {
// sharing without permissions should automatically set the permission to 15 for share and 1 for public link
$fd['permissions'] = 15;
}
if (!\in_array($ocsApiVersion, [1, 2], true)) {
throw new InvalidArgumentException(
"invalid ocsApiVersion ($ocsApiVersion)"
);
}
if (!\in_array($sharingApiVersion, [1, 2], true)) {
throw new InvalidArgumentException(
"invalid sharingApiVersion ($sharingApiVersion)"
);
}
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= "ocs/v$ocsApiVersion.php/apps/$sharingApp/api/v$sharingApiVersion/shares";
$fd['path'] = $path;
$fd['shareType'] = self::getShareType($shareType);
if ($shareWith !== null) {
$fd['shareWith'] = $shareWith;
}
if ($publicUpload !== null) {
$fd['publicUpload'] = $publicUpload;
}
if ($sharePassword !== null) {
$fd['password'] = $sharePassword;
}
if ($linkName !== null) {
$fd['name'] = $linkName;
}
if ($expireDate !== null) {
$fd['expireDate'] = $expireDate;
}
if ($space_ref !== null) {
$fd['space_ref'] = $space_ref;
}
$headers = ['OCS-APIREQUEST' => 'true'];
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$fd
);
}
/**
* Calculates the permission sum (int) from the given permissions.
* Permissions can be passed in as int, string or array of int or string
* 'read' => 1
* 'update' => 2
* 'create' => 4
* 'delete' => 8
* 'share' => 16
* 'invite' => 0
*
* @param string[]|string|int|int[] $permissions
*
* @return int
* @throws InvalidArgumentException
*
*/
public static function getPermissionSum($permissions): int {
if (\is_numeric($permissions)) {
// Allow any permission number so that test scenarios can
// specifically test invalid permission values
return (int) $permissions;
}
if (!\is_array($permissions)) {
$permissions = [$permissions];
}
$permissionSum = 0;
foreach ($permissions as $permission) {
if (\array_key_exists($permission, self::PERMISSION_TYPES)) {
$permissionSum += self::PERMISSION_TYPES[$permission];
} elseif (\in_array($permission, self::PERMISSION_TYPES, true)) {
$permissionSum += $permission;
} else {
throw new InvalidArgumentException(
"invalid permission type ($permission)"
);
}
}
if ($permissionSum < 0 || $permissionSum > 31) {
throw new InvalidArgumentException(
"invalid permission total ($permissionSum)"
);
}
return $permissionSum;
}
/**
* returns the share type number corresponding to the share type keyword
*
* @param string|int $shareType a keyword from SHARE_TYPES or a share type number
*
* @return int
* @throws InvalidArgumentException
*
*/
public static function getShareType($shareType): int {
if (\array_key_exists($shareType, self::SHARE_TYPES)) {
return self::SHARE_TYPES[$shareType];
} else {
if (\ctype_digit($shareType)) {
$shareType = (int) $shareType;
}
$key = \array_search($shareType, self::SHARE_TYPES, true);
if ($key !== false) {
return self::SHARE_TYPES[$key];
}
throw new InvalidArgumentException(
"invalid share type ($shareType)"
);
}
}
/**
*
* @param SimpleXMLElement $responseXmlObject
* @param string $errorMessage
*
* @return string
* @throws Exception
*
*/
public static function getLastShareIdFromResponse(
SimpleXMLElement $responseXmlObject,
string $errorMessage = "cannot find share id in response"
): string {
$xmlPart = $responseXmlObject->xpath("//data/element[last()]/id");
if (!\is_array($xmlPart) || (\count($xmlPart) === 0)) {
throw new Exception($errorMessage);
}
return $xmlPart[0]->__toString();
}
}
@@ -0,0 +1,14 @@
<?php
/**
* @author Sagar Gurung <sagar@jankatitech.com>
*
*/
namespace TestHelpers;
use Exception;
/**
* Class SpaceNotFoundException
* Exception when space id for a user is not found
*/
class SpaceNotFoundException extends Exception {
}
@@ -0,0 +1,373 @@
<?php
/**
* @author Viktor Scharf <v.scharf@qsfera.eu>
* @copyright Copyright (c) 2025 Viktor Scharf <v.scharf@qsfera.eu>
*
* 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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
/**
* Helper for obtaining bearer tokens for users
*/
class TokenHelper {
private const LOGON_URL = '/signin/v1/identifier/_/logon';
private const REDIRECT_URL = '/oidc-callback.html';
private const TOKEN_URL = '/konnect/v1/token';
// Static cache [username => token_data]
private static array $tokenCache = [];
/**
* @return bool
*/
public static function useBearerToken(): bool {
return \getenv('USE_BEARER_TOKEN') === 'true';
}
/**
* Extracts base URL from a full URL
*
* @param string $url
*
* @return string the base URL
*/
private static function extractBaseUrl(string $url): string {
return preg_replace('#(https?://[^/]+).*#', '$1', $url);
}
/**
* Get access and refresh tokens for a user
* Uses cache to avoid unnecessary requests
*
* @param string $username
* @param string $password
* @param string $url
*
* @return array ['access_token' => string, 'refresh_token' => string, 'expires_at' => int]
* @throws GuzzleException
* @throws Exception
*/
public static function getTokens(string $username, string $password, string $url): array {
// Extract base URL. I need to send $url to get correct server in case of multiple servers (ocm suite)
$baseUrl = self::extractBaseUrl($url);
$cacheKey = $username . '|' . $baseUrl;
// Check cache
if (isset(self::$tokenCache[$cacheKey])) {
$cachedToken = self::$tokenCache[$cacheKey];
// Check if access token has expired
if (time() < $cachedToken['expires_at']) {
return $cachedToken;
}
$refreshedToken = self::refreshToken($cachedToken['refresh_token'], $baseUrl);
$tokenData = [
'access_token' => $refreshedToken['access_token'],
'refresh_token' => $refreshedToken['refresh_token'],
// set expiry to 240 (4 minutes) seconds to allow for some buffer
// token actually expires in 300 seconds (5 minutes)
'expires_at' => time() + 240
];
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
}
// Get new tokens
$cookieJar = new CookieJar();
$continueUrl = self::getAuthorizedEndPoint($username, $password, $baseUrl, $cookieJar);
$code = self::getCode($continueUrl, $baseUrl, $cookieJar);
$tokens = self::getToken($code, $baseUrl, $cookieJar);
$tokenData = [
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
// set expiry to 240 (4 minutes) seconds to allow for some buffer
// token actually expires in 300 seconds (5 minutes)
'expires_at' => time() + 240
];
// Save to cache
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
}
/**
* Refresh token
*
* @param string $refreshToken
* @param string $baseUrl
*
* @return array
* @throws GuzzleException
* @throws Exception
*/
private static function refreshToken(string $refreshToken, string $baseUrl): array {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false
]
);
$response = $client->post(
$baseUrl . self::TOKEN_URL,
[
'form_params' => [
'client_id' => 'web',
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
]
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Token refresh failed: Expected status code 200 but received ' . $response->getStatusCode()
);
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey('access_token', $data, 'Missing access_token in refresh response');
Assert::assertArrayHasKey('refresh_token', $data, 'Missing refresh_token in refresh response');
return [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token']
];
}
/**
* Clear cached tokens for a specific user
*
* @param string $username
* @param string $url
*
* @return void
*/
public static function clearUserTokens(string $username, string $url): void {
$baseUrl = self::extractBaseUrl($url);
$cacheKey = $username . '|' . $baseUrl;
unset(self::$tokenCache[$cacheKey]);
}
/**
* Clear all cached tokens
*
* @return void
*/
public static function clearAllTokens(): void {
self::$tokenCache = [];
}
/**
* @param string $username
* @param string $password
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return \Psr\Http\Message\ResponseInterface
* @throws GuzzleException
*/
public static function makeLoginRequest(
string $username,
string $password,
string $baseUrl,
CookieJar $cookieJar
): \Psr\Http\Message\ResponseInterface {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false,
'cookies' => $cookieJar
]
);
return $client->post(
$baseUrl . self::LOGON_URL,
[
'headers' => [
'Kopano-Konnect-XSRF' => '1',
'Referer' => $baseUrl,
'Content-Type' => 'application/json'
],
'json' => [
'params' => [$username, $password, '1'],
'hello' => [
'scope' => 'openid profile offline_access email',
'client_id' => 'web',
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'flow' => 'oidc'
]
]
]
);
}
/**
* Step 1: Login and get continue_uri
*
* @param string $username
* @param string $password
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
private static function getAuthorizedEndPoint(
string $username,
string $password,
string $baseUrl,
CookieJar $cookieJar
): string {
$response = self::makeLoginRequest($username, $password, $baseUrl, $cookieJar);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Logon failed: Expected status code 200 but received: ' . $response->getStatusCode()
);
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey(
'hello',
$data,
'Logon response does not contain "hello" object'
);
Assert::assertArrayHasKey(
'continue_uri',
$data['hello'],
'Missing continue_uri in logon response'
);
return $data['hello']['continue_uri'];
}
/**
* Step 2: Authorization and get code
*
* @param string $continueUrl
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
private static function getCode(string $continueUrl, string $baseUrl, CookieJar $cookieJar): string {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false, // Disable automatic redirects
'cookies' => $cookieJar
]
);
$params = [
'client_id' => 'web',
'prompt' => 'none',
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'response_mode' => 'query',
'response_type' => 'code',
'scope' => 'openid profile offline_access email'
];
$response = $client->get(
$continueUrl,
[
'query' => $params
]
);
Assert::assertEquals(
302,
$response->getStatusCode(),
'Authorization request failed: Expected status code 302 but received: ' . $response->getStatusCode()
);
$location = $response->getHeader('Location')[0] ?? '';
Assert::assertNotEmpty($location, 'Missing Location header in authorization response');
parse_str(parse_url($location, PHP_URL_QUERY), $queryParams);
Assert::assertArrayHasKey('code', $queryParams, 'Missing code parameter in redirect URL');
return $queryParams['code'];
}
/**
* Step 3: Get token
*
* @param string $code
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*
*/
private static function getToken(string $code, string $baseUrl, CookieJar $cookieJar): array {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false,
'cookies' => $cookieJar
]
);
$response = $client->post(
$baseUrl . self::TOKEN_URL,
[
'form_params' => [
'client_id' => 'web',
'code' => $code,
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'grant_type' => 'authorization_code'
]
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Token request failed: Expected status code 200 but received: ' . $response->getStatusCode()
);
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey('access_token', $data, 'Missing access_token in token response');
Assert::assertArrayHasKey('refresh_token', $data, 'Missing refresh_token in token response');
return [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token']
];
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types=1);
/**
* @author Talank Baral <talank@jankaritech.com>
* @copyright Copyright (c) 2021 Talank Baral talank@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/>
*
*/
namespace TestHelpers;
/**
* Class TranslationHelper
*
* Helper functions that are needed to run tests in different languages
*
* @package TestHelpers
*/
class TranslationHelper {
/**
* @param string|null $language
*
* @return string|null
*/
public static function getLanguage(?string $language): ?string {
if (!isset($language)) {
if (\getenv('OC_LANGUAGE') !== false) {
$language = \getenv('OC_LANGUAGE');
}
}
return $language;
}
}
@@ -0,0 +1,214 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
/**
* Helper for Uploads
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class UploadHelper extends Assert {
/**
*
* @param string|null $baseUrl URL of КуСфера
* @param string|null $user
* @param string|null $password
* @param string|null $source
* @param string|null $destination
* @param string|null $xRequestId
* @param array|null $headers
* @param int|null $davPathVersionToUse (1|2)
* @param bool $doChunkUpload
* @param int|null $noOfChunks how many chunks to upload
* @param bool|null $isGivenStep
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function upload(
?string $baseUrl,
?string $user,
?string $password,
?string $source,
?string $destination,
?string $xRequestId = '',
?array $headers = [],
?int $davPathVersionToUse = 1,
bool $doChunkUpload = false,
?int $noOfChunks = 1,
?bool $isGivenStep = false,
): ResponseInterface {
if (!$doChunkUpload) {
$data = \file_get_contents($source);
return WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PUT",
$destination,
$headers,
null,
$xRequestId,
$data,
$davPathVersionToUse,
"files",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
}
//prepare chunking
$chunks = self::chunkFile($source, $noOfChunks);
$chunkingId = 'chunking-' . \rand(1000, 9999);
$result = null;
//upload chunks
foreach ($chunks as $index => $chunk) {
$filename = $destination . "-" . $chunkingId . "-" . \count($chunks) . '-' . $index;
$result = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PUT",
$filename,
$headers,
null,
$xRequestId,
$chunk,
$davPathVersionToUse,
"files",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
if ($result->getStatusCode() >= 400) {
return $result;
}
}
self::assertNotNull($result, __METHOD__ . " chunking was requested but no upload was done.");
return $result;
}
/**
* cut the file in multiple chunks
* returns an array of chunks with the content of the file
*
* @param string|null $file
* @param int|null $noOfChunks
*
* @return array $string
*/
public static function chunkFile(?string $file, ?int $noOfChunks = 1): array {
$size = \filesize($file);
$chunkSize = \ceil($size / $noOfChunks);
$chunks = [];
$fp = \fopen($file, 'r');
while (!\feof($fp) && \ftell($fp) < $size) {
$chunks[] = \fread($fp, (int)$chunkSize);
}
\fclose($fp);
if (\count($chunks) === 0) {
// chunk an empty file
$chunks[] = '';
}
return $chunks;
}
/**
* creates a File with a specific size
*
* @param string|null $name full path of the file to create
* @param int|null $size
*
* @return void
*/
public static function createFileSpecificSize(?string $name, ?int $size): void {
if (\file_exists($name)) {
\unlink($name);
}
$file = \fopen($name, 'w');
\fseek($file, \max($size - 1, 0), SEEK_CUR);
if ($size) {
\fwrite($file, 'a'); // write a dummy char at SIZE position
}
\fclose($file);
self::assertEquals(
1,
\file_exists($name)
);
self::assertEquals(
$size,
\filesize($name)
);
}
/**
* creates a File with a specific text content
*
* @param string|null $name full path of the file to create
* @param string|null $text
*
* @return void
*/
public static function createFileWithText(?string $name, ?string $text): void {
$file = \fopen($name, 'w');
\fwrite($file, $text);
\fclose($file);
self::assertEquals(
1,
\file_exists($name)
);
}
/**
* get the path of the acceptance tests directory
*
* @return string
*/
public static function getAcceptanceTestsDir(): string {
return \dirname(__FILE__) . "/../";
}
/**
* get the path of the filesForUpload directory
*
* @return string
*/
public static function getFilesForUploadDir(): string {
return \dirname(__FILE__) . "/../filesForUpload/";
}
}
@@ -0,0 +1,317 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* Helper to administrate users (and groups) through the provisioning API
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class UserHelper {
/**
*
* @param string|null $baseUrl
* @param string $user
* @param string $key
* @param string $value
* @param string $adminUser
* @param string $adminPassword
* @param string $xRequestId
* @param int|null $ocsApiVersion
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function editUser(
?string $baseUrl,
string $user,
string $key,
string $value,
string $adminUser,
string $adminPassword,
string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"PUT",
"/cloud/users/" . $user,
$xRequestId,
["key" => $key, "value" => $value],
$ocsApiVersion
);
}
/**
* Send batch requests to edit the user.
* This will send multiple requests in parallel to the server which will be faster in comparison to waiting for each request to complete.
*
* @param string|null $baseUrl
* @param array|null $editData ['user' => '', 'key' => '', 'value' => '']
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion
*
* @return array
* @throws Exception
*/
public static function editUserBatch(
?string $baseUrl,
?array $editData,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): array {
$requests = [];
$client = HttpRequestHelper::createClient(
$adminUser,
$adminPassword
);
foreach ($editData as $data) {
$path = "/cloud/users/" . $data['user'];
$body = ["key" => $data['key'], 'value' => $data["value"]];
// Create the OCS API requests and push them to an array.
$requests[] = OcsApiHelper::createOcsRequest(
$baseUrl,
'PUT',
$path,
$xRequestId,
$body
);
}
// Send the array of requests at once in parallel.
$results = HttpRequestHelper::sendBatchRequest($requests, $client);
foreach ($results as $e) {
if ($e instanceof ClientException) {
$httpStatusCode = $e->getResponse()->getStatusCode();
$reasonPhrase = $e->getResponse()->getReasonPhrase();
throw new Exception(
"Unexpected failure when editing a user: HTTP status $httpStatusCode HTTP reason $reasonPhrase"
);
}
}
return $results;
}
/**
*
* @param string|null $baseUrl
* @param string|null $userName
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function getUser(
?string $baseUrl,
?string $userName,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"GET",
"/cloud/users/" . $userName,
$xRequestId,
[],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $userName
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteUser(
?string $baseUrl,
?string $userName,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"DELETE",
"/cloud/users/" . $userName,
$xRequestId,
[],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $group
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion (1|2)
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function addUserToGroup(
?string $baseUrl,
?string $user,
?string $group,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"POST",
"/cloud/users/" . $user . "/groups",
$xRequestId,
['groupid' => $group],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $group
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion (1|2)
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function removeUserFromGroup(
?string $baseUrl,
?string $user,
?string $group,
?string $adminUser,
?string $adminPassword,
?string $xRequestId,
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"DELETE",
"/cloud/users/" . $user . "/groups",
$xRequestId,
['groupid' => $group],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param string|null $search
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function getGroups(
?string $baseUrl,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?string $search =""
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"GET",
"/cloud/groups?search=" . $search,
$xRequestId
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param string|null $search
*
* @return string[]
* @throws Exception|GuzzleException
*/
public static function getGroupsAsArray(
?string $baseUrl,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?string $search = ""
): array {
$result = self::getGroups(
$baseUrl,
$adminUser,
$adminPassword,
$xRequestId,
$search
);
$groups = HttpRequestHelper::getResponseXml($result, __METHOD__)->xpath(".//groups")[0];
$return = [];
foreach ($groups as $group) {
$return[] = $group->__toString();
}
return $return;
}
}
@@ -0,0 +1,967 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@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/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use DateTime;
/**
* Helper to make WebDav Requests
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class WebDavHelper {
public const DAV_VERSION_OLD = 1;
public const DAV_VERSION_NEW = 2;
public const DAV_VERSION_SPACES = 3;
/**
* @var array of users with their different space ids
*/
public static array $spacesIdRef = [];
/**
* @return bool
*/
public static function withRemotePhp(): bool {
// use remote.php by default
return \getenv("WITH_REMOTE_PHP") !== "false";
}
/**
* @param string $urlPath
*
* @return string
*/
public static function prefixRemotePhp(string $urlPath): string {
if (self::withRemotePhp()) {
return "remote.php/$urlPath";
}
return $urlPath;
}
/**
* @param string $url
*
* @return bool
*/
public static function isDAVRequest(string $url): bool {
$found = \preg_match("/(\bwebdav\b|\bdav\b)/", $url);
return (bool)$found;
}
/**
* clear space id reference for user
*
* @param string|null $user
*
* @return void
* @throws Exception
*/
public static function removeSpaceIdReferenceForUser(
?string $user
): void {
if (\array_key_exists($user, self::$spacesIdRef)) {
unset(self::$spacesIdRef[$user]);
}
}
/**
* @param string $namespaceString
*
* @return object
*/
public static function parseNamespace(string $namespaceString): object {
//calculate the namespace prefix and namespace
$matches = [];
\preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches);
return (object)["namespace" => $matches[2], "prefix" => $matches[1]];
}
/**
* returns the id of a file
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string|null $spaceId
* @param string|null $xRequestId
* @param int|null $davPathVersionToUse
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getFileIdForPath(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?string $spaceId = null,
?string $xRequestId = '',
?int $davPathVersionToUse = self::DAV_VERSION_NEW
): string {
$body
= '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:fileid />
</d:prop>
</d:propfind>';
$response = self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$path,
null,
$spaceId,
$xRequestId,
$body,
$davPathVersionToUse
);
\preg_match(
'/\<oc:fileid\>([^\<]*)\<\/oc:fileid\>/',
$response->getBody()->getContents(),
$matches
);
if (!isset($matches[1])) {
throw new Exception("could not find fileId of $path");
}
return $matches[1];
}
/**
* returns body for propfind
*
* @param array|null $properties
*
* @return string
* @throws Exception
*/
public static function getBodyForPropfind(?array $properties): string {
$propertyBody = "";
$extraNamespaces = "";
foreach ($properties as $namespaceString => $property) {
if (\is_int($namespaceString)) {
//default namespace prefix if the property has no array key
//also used if no prefix is given in the property value
$namespacePrefix = null;
} else {
$ns = self::parseNamespace($namespaceString);
$namespacePrefix = $ns->prefix;
$extraNamespaces .= " xmlns:$namespacePrefix=\"$ns->namespace\" ";
}
//if a namespace prefix is given in the property value use that
if (\strpos($property, ":") !== false) {
$propertyParts = \explode(":", $property);
$namespacePrefix = $propertyParts[0];
$property = $propertyParts[1];
}
if ($namespacePrefix) {
$propertyBody .= "<$namespacePrefix:$property/>";
} else {
$propertyBody .= "<$property/>";
}
}
$body = "<?xml version=\"1.0\"?>
<d:propfind
xmlns:d=\"DAV:\"
xmlns:oc=\"http://owncloud.org/ns\"
xmlns:ocs=\"http://open-collaboration-services.org/ns\"
$extraNamespaces>
<d:prop>$propertyBody</d:prop>
</d:propfind>";
return $body;
}
/**
* sends a PROPFIND request
* with these registered namespaces:
* | prefix | namespace |
* | d | DAV: |
* | oc | http://owncloud.org/ns |
* | ocs | http://open-collaboration-services.org/ns |
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string[] $properties
* string can contain namespace prefix,
* if no prefix is given 'd:' is used as prefix
* if an associative array is used, then the key will be used as namespace
* @param string|null $xRequestId
* @param string|null $folderDepth
* @param string|null $spaceId
* @param string|null $type
* @param int|null $davPathVersionToUse
* @param string|null $doDavRequestAsUser
* @param array|null $headers
*
* @return ResponseInterface
* @throws Exception
* @throws GuzzleException
*/
public static function propfind(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?array $properties,
?string $xRequestId = '',
?string $folderDepth = '1',
?string $spaceId = null,
?string $type = "files",
?int $davPathVersionToUse = self::DAV_VERSION_NEW,
?string $doDavRequestAsUser = null,
?array $headers = []
): ResponseInterface {
$body = self::getBodyForPropfind($properties);
$folderDepth = (string) $folderDepth;
if ($folderDepth !== '0' && $folderDepth !== '1' && $folderDepth !== 'infinity') {
if ($folderDepth !== '') {
throw new InvalidArgumentException('Invalid depth value ' . $folderDepth);
}
$folderDepth = '1'; // КуСфера server's default value
}
$headers['Depth'] = $folderDepth;
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$path,
$headers,
$spaceId,
$xRequestId,
$body,
$davPathVersionToUse,
$type,
null,
null,
false,
null,
null,
[],
$doDavRequestAsUser
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string|null $propertyName
* @param string|null $propertyValue
* @param string|null $xRequestId
* @param string|null $namespaceString string containing prefix and namespace
* e.g "x1='http://whatever.org/ns'"
* @param int|null $davPathVersionToUse
* @param string|null $type
* @param string|null $spaceId
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function proppatch(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?string $propertyName,
?string $propertyValue,
?string $xRequestId = '',
?string $namespaceString = null,
?int $davPathVersionToUse = self::DAV_VERSION_NEW,
?string $type="files",
?string $spaceId = null,
): ResponseInterface {
if ($namespaceString !== null) {
$ns = self::parseNamespace($namespaceString);
$propertyBody = "<$ns->prefix:$propertyName" .
" xmlns:$ns->prefix=\"$ns->namespace\">" .
"$propertyValue" .
"</$ns->prefix:$propertyName>";
} else {
$propertyBody = "<$propertyName>$propertyValue</$propertyName>";
}
$body = "<?xml version=\"1.0\"?>
<d:propertyupdate xmlns:d=\"DAV:\"
xmlns:oc=\"http://owncloud.org/ns\">
<d:set>
<d:prop>$propertyBody</d:prop>
</d:set>
</d:propertyupdate>";
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPPATCH",
$path,
[],
$spaceId,
$xRequestId,
$body,
$davPathVersionToUse,
$type
);
}
/**
* gets namespace-prefix, namespace url and propName from provided namespaceString or property
* or otherwise use default
*
* @param string $namespaceString
* @param string $property
*
* @return array
*/
public static function getPropertyWithNamespaceInfo(string $namespaceString = "", string $property = ""): array {
$namespace = "";
$namespacePrefix = "";
if (\is_int($namespaceString)) {
//default namespace prefix if the property has no array key
//also used if no prefix is given in the property value
$namespacePrefix = "d";
$namespace = "DAV:";
} elseif ($namespaceString) {
$ns = self::parseNamespace($namespaceString);
$namespacePrefix = $ns->prefix;
$namespace = $ns->namespace;
}
//if a namespace prefix is given in the property value use that
if ($property && \strpos($property, ":")) {
$propertyParts = \explode(":", $property);
$namespacePrefix = $propertyParts[0];
$property = $propertyParts[1];
}
return [$namespacePrefix, $namespace, $property];
}
/**
* sends HTTP request PROPPATCH method with multiple properties
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string $path
* @param array|null $propertiesArray
* @param string|null $xRequestId
* @param int|null $davPathVersion
* @param string|null $namespaceString
* @param string|null $type
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function proppatchWithMultipleProps(
?string $baseUrl,
?string $user,
?string $password,
string $path,
?array $propertiesArray,
?string $xRequestId = '',
?int $davPathVersion = null,
?string $namespaceString = null,
?string $type="files"
): ResponseInterface {
$propertyBody = "";
foreach ($propertiesArray as $propertyArray) {
$property = $propertyArray["propertyName"];
$value = $propertyArray["propertyValue"];
if ($namespaceString !== null) {
$matches = [];
[$namespacePrefix, $namespace, $property] = self::getPropertyWithNamespaceInfo(
$namespaceString,
$property
);
$propertyBody .= "\n\t<$namespacePrefix:$property>" .
"$value" .
"</$namespacePrefix:$property>";
} else {
$propertyBody .= "<$property>$value</$property>";
}
}
$body = "<?xml version=\"1.0\"?>
<d:propertyupdate xmlns:d=\"DAV:\"
xmlns:oc=\"http://owncloud.org/ns\">
<d:set>
<d:prop>$propertyBody
</d:prop>
</d:set>
</d:propertyupdate>";
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPPATCH",
$path,
[],
null,
$xRequestId,
$body,
$davPathVersion,
$type
);
}
/**
* returns the response to listing a folder (collection)
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string|null $folderDepth
* @param string|null $spaceId
* @param string|null $xRequestId
* @param string[] $properties
* @param string|null $type
* @param int|null $davPathVersionToUse
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function listFolder(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?string $folderDepth,
?string $spaceId = null,
?string $xRequestId = '',
?array $properties = null,
?string $type = "files",
?int $davPathVersionToUse = self::DAV_VERSION_NEW
): ResponseInterface {
if (!$properties) {
$properties = [
'd:getetag', 'd:resourcetype'
];
}
return self::propfind(
$baseUrl,
$user,
$password,
$path,
$properties,
$xRequestId,
$folderDepth,
$spaceId,
$type,
$davPathVersionToUse
);
}
/**
* Generates UUIDv4
* Example: 123e4567-e89b-12d3-a456-426614174000
*
* @return string
* @throws Exception
*/
public static function generateUUIDv4(): string {
// generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = random_bytes(16);
\assert(\strlen($data) == 16);
$data[6] = \chr(\ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = \chr(\ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
/**
* fetches personal space id for provided user
*
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function getPersonalSpaceIdForUser(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): string {
if (\array_key_exists($user, self::$spacesIdRef) && \array_key_exists("personal", self::$spacesIdRef[$user])) {
return self::$spacesIdRef[$user]["personal"];
}
$personalSpaceId = '';
if (!OcHelper::isTestingOnReva()) {
$response = GraphHelper::getMySpaces($baseUrl, $user, $password, '', $xRequestId);
Assert::assertEquals(200, $response->getStatusCode(), "Cannot list drives for user '$user'");
$drives = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
foreach ($drives->value as $drive) {
if ($drive->driveType === "personal") {
$personalSpaceId = $drive->id;
break;
}
}
}
if (!$personalSpaceId) {
// the graph endpoint did not give a useful answer
// try getting the information from the webdav endpoint
$fullUrl = "$baseUrl/" . self::getDavPath(self::DAV_VERSION_NEW, $user);
$response = HttpRequestHelper::sendRequest(
$fullUrl,
$xRequestId,
'PROPFIND',
$user,
$password
);
Assert::assertEquals(
207,
$response->getStatusCode(),
"PROPFIND for user '$user' failed so the personal space id cannot be discovered"
);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("/d:multistatus/d:response[1]/d:propstat/d:prop/oc:spaceid");
Assert::assertNotEmpty(
$xmlPart,
"The 'oc:spaceid' for user '$user' was not found in the PROPFIND response"
);
$personalSpaceId = $xmlPart[0]->__toString();
}
Assert::assertNotEmpty($personalSpaceId, "The personal space id for user '$user' was not found");
self::$spacesIdRef[$user] = [];
self::$spacesIdRef[$user]["personal"] = $personalSpaceId;
return $personalSpaceId;
}
/**
* First checks if a user exists to return its space ID
* In case of any exception, it returns a fake space ID
*
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getPersonalSpaceIdForUserOrFakeIfNotFound(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): string {
if (\str_starts_with($user, "non-exist") || \str_starts_with($user, "nonexist")) {
return self::generateUUIDv4();
}
return self::getPersonalSpaceIdForUser(
$baseUrl,
$user,
$password,
$xRequestId,
);
}
/**
* sends a DAV request
*
* @param string|null $baseUrl URL of КуСфера e.g. http://localhost:8080
* @param string|null $user
* @param string|null $password or token when bearer auth is used
* @param string|null $method PUT, GET, DELETE, etc.
* @param string|null $path
* @param array|null $headers
* @param string|null $spaceId
* @param string|null $xRequestId
* @param string|null|resource|StreamInterface $body
* @param int|null $davPathVersionToUse (1|2|3)
* @param string|null $type of request
* @param string|null $sourceIpAddress to initiate the request from
* @param string|null $authType basic|bearer
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
* @param array|null $urlParameter to concatenate with path
* @param string|null $doDavRequestAsUser run the DAV as this user, if null it is the same as $user
* @param bool $isGivenStep is set to true if makeDavRequest is called from a "given" step
*
* @return ResponseInterface
* @throws GuzzleException
* @throws Exception
*/
public static function makeDavRequest(
?string $baseUrl,
?string $user,
?string $password,
?string $method,
?string $path,
?array $headers,
?string $spaceId = null,
?string $xRequestId = '',
$body = null,
?int $davPathVersionToUse = self::DAV_VERSION_OLD,
?string $type = "files",
?string $sourceIpAddress = null,
?string $authType = "basic",
?bool $stream = false,
?int $timeout = 0,
?Client $client = null,
?array $urlParameter = [],
?string $doDavRequestAsUser = null,
?bool $isGivenStep = false,
): ResponseInterface {
$baseUrl = self::sanitizeUrl($baseUrl, true);
// We need to manipulate and use path as a string.
// So ensure that it is a string to avoid any type-conversion errors.
if ($path === null) {
$path = "";
}
// get space id if testing with spaces dav
if ($spaceId === null && $davPathVersionToUse === self::DAV_VERSION_SPACES
&& !\in_array($type, ["public-files", "versions"])
) {
$path = \ltrim($path, "/");
if (\str_starts_with($path, "Shares/")) {
$spaceId = GraphHelper::SHARES_SPACE_ID;
$path = "/" . preg_replace("/^Shares\//", "", $path);
} else {
$spaceId = self::getPersonalSpaceIdForUserOrFakeIfNotFound(
$baseUrl,
$user,
$password,
$xRequestId
);
}
}
$suffixPath = $user;
if ($davPathVersionToUse === self::DAV_VERSION_SPACES
&& !\in_array($type, ["archive", "versions", "public-files"])
) {
$suffixPath = $spaceId;
} elseif ($type === "versions") {
// $path is file-id in case of versions
$suffixPath = $path;
}
$davPath = self::getDavPath($davPathVersionToUse, $suffixPath, $type);
//replace %, # and ? and in the path, Guzzle will not encode them
$urlSpecialChar = [['%', '#', '?'], ['%25', '%23', '%3F']];
$path = \str_replace($urlSpecialChar[0], $urlSpecialChar[1], $path);
if (!empty($urlParameter)) {
$urlParameter = \http_build_query($urlParameter, '', '&');
$path .= '?' . $urlParameter;
}
$fullUrl = self::sanitizeUrl("$baseUrl/$davPath");
// NOTE: no need to append path for archive and versions endpoints
if (!\in_array($type, ["archive", "versions"])) {
$fullUrl .= "/" . \ltrim($path, "/");
}
if ($authType === 'bearer') {
$headers['Authorization'] = 'Bearer ' . $password;
$user = null;
$password = null;
}
if ($type === "public-files") {
if ($password === null || $password === "") {
$user = null;
} else {
$user = "public";
}
}
$config = null;
if ($sourceIpAddress !== null) {
$config = [ 'curl' => [ CURLOPT_INTERFACE => $sourceIpAddress ]];
}
if ($headers !== null) {
foreach ($headers as $key => $value) {
//? and # need to be encoded in the Destination URL
if ($key === "Destination") {
$headers[$key] = \str_replace(
$urlSpecialChar[0],
$urlSpecialChar[1],
$value
);
break;
}
}
}
return HttpRequestHelper::sendRequest(
$fullUrl,
$xRequestId,
$method,
$doDavRequestAsUser ?? $user,
$password,
$headers,
$body,
$config,
null,
$stream,
$timeout,
$client,
$isGivenStep
);
}
/**
* get the dav path
*
* @param int $davPathVersion (1|2|3)
* @param string|null $userOrItemIdOrSpaceIdOrToken 'user' or 'file-id' or 'space-id' or 'public-token'
* @param string|null $type
*
* @return string
*/
public static function getDavPath(
int $davPathVersion,
?string $userOrItemIdOrSpaceIdOrToken = null,
?string $type = "files"
): string {
switch ($type) {
case 'archive':
return self::prefixRemotePhp("dav/archive/$userOrItemIdOrSpaceIdOrToken/files");
case 'versions':
return self::prefixRemotePhp("dav/meta/$userOrItemIdOrSpaceIdOrToken/v");
case 'comments':
return self::prefixRemotePhp("dav/comments/files");
default:
break;
}
if ($davPathVersion === self::DAV_VERSION_SPACES) {
if ($type === "trash-bin") {
if ($userOrItemIdOrSpaceIdOrToken === null) {
throw new InvalidArgumentException("Space ID is required for trash-bin endpoint");
}
return self::prefixRemotePhp("dav/spaces/trash-bin/$userOrItemIdOrSpaceIdOrToken");
} elseif ($type === "public-files") {
// spaces DAV path doesn't have own public-files endpoint
return self::prefixRemotePhp("dav/public-files/$userOrItemIdOrSpaceIdOrToken");
}
// return spaces root path if spaceid is null
// REPORT request uses spaces root path
if ($userOrItemIdOrSpaceIdOrToken === null) {
return self::prefixRemotePhp("dav/spaces");
}
return self::prefixRemotePhp("dav/spaces/$userOrItemIdOrSpaceIdOrToken");
} else {
if ($type === "trash-bin") {
// Since there is no trash bin endpoint for old dav version,
// new dav version's endpoint is used here.
return self::prefixRemotePhp("dav/trash-bin/$userOrItemIdOrSpaceIdOrToken");
}
if ($davPathVersion === self::DAV_VERSION_OLD) {
return self::prefixRemotePhp("webdav");
} elseif ($davPathVersion === self::DAV_VERSION_NEW) {
if ($type === "files") {
return self::prefixRemotePhp("dav/files/$userOrItemIdOrSpaceIdOrToken");
} elseif ($type === "public-files") {
return self::prefixRemotePhp("dav/public-files/$userOrItemIdOrSpaceIdOrToken");
}
return self::prefixRemotePhp("dav");
}
throw new InvalidArgumentException("Invalid DAV path: $davPathVersion");
}
}
/**
* make sure there are no double-slashes in the URL
*
* @param string|null $url
* @param bool|null $trailingSlash forces a trailing slash
*
* @return string
*/
public static function sanitizeUrl(?string $url, ?bool $trailingSlash = false): string {
if ($trailingSlash === true) {
$url = $url . "/";
} else {
$url = \rtrim($url, "/");
}
return \preg_replace("/([^:]\/)\/+/", '$1', $url);
}
/**
* get Mtime of File in a public link share
*
* @param string|null $baseUrl
* @param string|null $fileName
* @param string|null $token
* @param string|null $xRequestId
* @param int|null $davVersionToUse
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getMtimeOfFileInPublicLinkShare(
?string $baseUrl,
?string $fileName,
?string $token,
?string $xRequestId = '',
?int $davVersionToUse = self::DAV_VERSION_NEW
): string {
$response = self::propfind(
$baseUrl,
null,
null,
"$token/$fileName",
['d:getlastmodified'],
$xRequestId,
'1',
null,
"public-files",
$davVersionToUse
);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:getlastmodified");
return $xmlPart[0]->__toString();
}
/**
* get Mtime of a resource
*
* @param string|null $user
* @param string|null $password
* @param string|null $baseUrl
* @param string|null $resource
* @param string|null $xRequestId
* @param int|null $davPathVersionToUse
* @param string|null $spaceId
*
* @return string
* @throws Exception
* @throws GuzzleException
*/
public static function getMtimeOfResource(
?string $user,
?string $password,
?string $baseUrl,
?string $resource,
?string $xRequestId = '',
?int $davPathVersionToUse = self::DAV_VERSION_NEW,
?string $spaceId = null,
): string {
$response = self::propfind(
$baseUrl,
$user,
$password,
$resource,
["d:getlastmodified"],
$xRequestId,
"0",
$spaceId,
"files",
$davPathVersionToUse
);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:getlastmodified");
Assert::assertArrayHasKey(
0,
$xmlPart,
__METHOD__
. " XML part does not have key 0. Expected a value at index 0 of 'xmlPart' but, found: "
. json_encode($xmlPart)
);
$mtime = new DateTime($xmlPart[0]->__toString());
return $mtime->format('U');
}
/**
* wait until the reqeust doesn't return 425 anymore
*
* @param string $url
* @param ?string $user
* @param ?string $password
* @param ?array $headers
*
* @return void
*/
public static function waitForPostProcessingToFinish(
string $url,
?string $user = null,
?string $password = null,
?array $headers = [],
): void {
$retried = 0;
do {
$response = HttpRequestHelper::sendRequest(
$url,
'check-425-status',
'GET',
$user,
$password,
$headers,
);
$statusCode = $response->getStatusCode();
if ($statusCode !== 425) {
return;
}
$tryAgain = $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "[INFO] Waiting for post processing to finish, attempt ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
echo "[ERROR] 10 seconds timeout! Post processing did not finish in time.\n";
}
}
@@ -0,0 +1,325 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2021 Artur Neumann artur@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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use splitbrain\PHPArchive\Tar;
use splitbrain\PHPArchive\Zip;
use splitbrain\PHPArchive\Archive;
require_once 'bootstrap.php';
/**
* Context for Archiver specific steps
*/
class ArchiverContext implements Context {
/**
* @var FeatureContext
*/
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*
* @throws Exception
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $query
*
* @return string
*/
public function getArchiverUrl(string $query): string {
return $this->featureContext->getBaseUrl() . '/archiver?' . $query;
}
/**
* @param string $type
*
* @return Archive
*/
public function getArchiveClass(string $type): Archive {
if ($type === 'zip') {
return new Zip();
} elseif ($type === 'tar') {
return new Tar();
} else {
throw new Exception('Unknown archive type: ' . $type);
}
}
/**
* @param string $dir
*
* @return void
*/
public function removeDir(string $dir): void {
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$itemPath = $dir . DIRECTORY_SEPARATOR . $item;
if (\is_dir($itemPath)) {
$this->removeDir($itemPath);
} else {
\unlink($itemPath);
}
}
\rmdir($dir);
}
/**
* @param string $user
* @param string $resource
* @param string $addressType id|ids|path|paths
*
* @return string
*
* @throws Exception
*/
private function getArchiverQueryString(
string $user,
string $resource,
string $addressType
): string {
switch ($addressType) {
case 'id':
case 'ids':
return 'id=' . $this->featureContext->getFileIdForPath($user, $resource);
case 'remoteItemIds':
return 'id=' . $this->spacesContext->getSharesRemoteItemId($user, $resource);
case 'path':
case 'paths':
return 'path=' . $resource;
default:
throw new Exception(
'"' . $addressType .
'" is not a legal value for $addressType, must be id|ids|remoteItemIds|path|paths'
);
}
}
/**
* @When /^user "([^"]*)" downloads the (zip|tar) archive of "([^"]*)" using the resource (id|ids|path|paths) and setting these headers:$/
*
* @param string $user
* @param string $archiveType
* @param string $resource
* @param string $addressType id|path
* @param TableNode $headersTable
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userDownloadsTheZipOrTarArchiveOfResourceUsingResourceIdOrPathAndSettingTheseHeaders(
string $user,
string $archiveType,
string $resource,
string $addressType,
TableNode $headersTable
): void {
$this->featureContext->verifyTableNodeColumns(
$headersTable,
['header', 'value']
);
$headers = [];
foreach ($headersTable as $row) {
$headers[$row['header']] = $row ['value'];
}
$this->featureContext->setResponse(
$this->downloadArchive($user, $resource, $addressType, $archiveType, null, $headers)
);
}
/**
* @When user :downloader downloads the archive of :item of user :owner using the resource :addressType
*
* @param string $downloader Who sends the request
* @param string $resource
* @param string $owner Who is the real owner of the file
* @param string $addressType id|path
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userDownloadsTheArchiveOfItemOfUser(
string $downloader,
string $resource,
string $owner,
string $addressType
): void {
$this->featureContext->setResponse($this->downloadArchive($downloader, $resource, $addressType, null, $owner));
}
/**
* @param string $downloader
* @param string $resource
* @param string $addressType
* @param string|null $archiveType
* @param string|null $owner
* @param array|null $headers
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function downloadArchive(
string $downloader,
string $resource,
string $addressType,
?string $archiveType = null,
?string $owner = null,
?array $headers = null
): ResponseInterface {
$owner = $owner ?? $downloader;
$downloader = $this->featureContext->getActualUsername($downloader);
$queryString = $this->getArchiverQueryString($owner, $resource, $addressType);
if ($archiveType !== null) {
$queryString .= '&output-format=' . $archiveType;
}
return HttpRequestHelper::get(
$this->getArchiverUrl($queryString),
$this->featureContext->getStepLineRef(),
$downloader,
$this->featureContext->getPasswordForUser($downloader),
$headers
);
}
/**
* @When user :user downloads the archive of these items using the resource :addressType
*
* @param string $user
* @param string $addressType ids|paths
* @param TableNode $items
*
* @return void
*
* @throws GuzzleException|Exception
*/
public function userDownloadsTheArchiveOfTheseItems(
string $user,
string $addressType,
TableNode $items
): void {
$user = $this->featureContext->getActualUsername($user);
$queryString = [];
foreach ($items->getRows() as $item) {
$queryString[] = $this->getArchiverQueryString($user, $item[0], $addressType);
}
$queryString = \join('&', $queryString);
$this->featureContext->setResponse(
HttpRequestHelper::get(
$this->getArchiverUrl($queryString),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
)
);
}
/**
* @Then the downloaded :type archive should contain these files:
*
* @param string $type
* @param TableNode $expectedFiles
*
* @return void
*
* @throws Exception
*/
public function theDownloadedArchiveShouldContainTheseFiles(string $type, TableNode $expectedFiles): void {
$this->featureContext->verifyTableNodeColumns($expectedFiles, ['name', 'content']);
$contents = $this->featureContext->getResponse()->getBody()->getContents();
$tempFile = \tempnam(\sys_get_temp_dir(), 'OcAcceptanceTests_');
$tempExtractFolder = $tempFile;
\unlink($tempFile); // we only need the name
$tempFile = $tempFile . '.' . $type; // it needs the extension
\file_put_contents($tempFile, $contents);
// open the archive
$tar = $this->getArchiveClass($type);
$tar->open($tempFile);
$archiveData = $tar->contents();
// extract the archive
$tar->open($tempFile);
$tar->extract($tempExtractFolder);
$tar->close();
foreach ($expectedFiles->getHash() as $expectedItem) {
$expectedPath = trim($expectedItem['name'], "/");
$found = false;
foreach ($archiveData as $info) {
// get only the parent folder path for the given item
$actualPath = $info->getPath();
if ($expectedPath === $actualPath) {
if (!$info->getIsdir()) {
$fileFullPath = "$tempExtractFolder/$actualPath";
$fileMimeType = \mime_content_type($fileFullPath);
if ($fileMimeType === "text/plain") {
$fileContent = \file_get_contents($fileFullPath);
Assert::assertEquals(
$expectedItem['content'],
$fileContent,
__METHOD__ . " content of '" . $expectedPath . "' not as expected"
);
} else {
Assert::assertFileExists(
$fileFullPath,
__METHOD__ . " File '" . $expectedPath . "' is not in the downloaded archive."
);
}
}
$found = true;
break;
}
}
if (!$found) {
Assert::fail("Resource '" . $expectedPath . "' is not in the downloaded archive.");
}
}
\unlink($tempFile);
$this->removeDir($tempExtractFolder);
}
}
@@ -0,0 +1,155 @@
<?php declare(strict_types=1);
/**
* @author Niraj Acharya <niraj@jankaritech.com>
* @copyright Copyright (c) 2024 Niraj Acharya niraj@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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use TestHelpers\BehatHelper;
use TestHelpers\AuthAppHelper;
require_once 'bootstrap.php';
/**
* AuthApp context
*/
class AuthAppContext implements Context {
private FeatureContext $featureContext;
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @When user :user creates app token with expiration time :expiration using the auth-app API
*
* @param string $user
* @param string $expiration
*
* @return void
*/
public function userCreatesAppTokenWithExpirationTimeUsingTheAuthAppApi(string $user, string $expiration): void {
$this->featureContext->setResponse(
AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
["expiry" => $expiration],
)
);
}
/**
* @Given user :user has created app token with expiration time :expiration using the auth-app API
*
* @param string $user
* @param string $expiration
*
* @return void
*/
public function userHasCreatedAppTokenWithExpirationTime(string $user, string $expiration): void {
$response = AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
["expiry" => $expiration]
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
}
/**
* @When user :user lists all created tokens using the auth-app API
*
* @param string $user
*
* @return void
*/
public function userListsAllCreatedTokensUsingTheAuthAppApi(string $user): void {
$this->featureContext->setResponse(
AuthAppHelper::listAllAppAuthTokensForUser(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
)
);
}
/**
* @Given the administrator has created app token for user :impersonatedUser with expiration time :expiration using the auth-app API
*
* @param string $impersonatedUser
* @param string $expiration
*
* @return void
*/
public function theAdministratorHasCreatedAppTokenWithExpirationTimeImpersonatingUserUsingTheAuthAppApi(
string $impersonatedUser,
string $expiration,
): void {
$response = AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getAdminUsername(),
$this->featureContext->getAdminPassword(),
[
"expiry" => $expiration,
"userName" => $this->featureContext->getActualUsername($impersonatedUser)
],
);
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"Failed creating auth-app token\n"
. "HTTP status code 200 is not the expected value " . $response->getStatusCode(),
$response
);
}
/**
* @When the administrator creates app token for user :impersonatedUser with expiration time :expiration using the auth-app API
*
* @param string $impersonatedUser
* @param string $expiration
*
* @return void
*/
public function theAdministratorCreatesAppTokenForUserWithExpirationTimeViaAuthAppApi(
string $impersonatedUser,
string $expiration,
): void {
$this->featureContext->setResponse(
AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getAdminUsername(),
$this->featureContext->getAdminPassword(),
[
"expiry" => $expiration,
"userName" => $this->featureContext->getActualUsername($impersonatedUser)
],
)
);
}
}
@@ -0,0 +1,741 @@
<?php declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@owncloud.com>
*
* @copyright Copyright (c) 2018, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Behat\Behat\Context\Context;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use TestHelpers\TokenHelper;
use TestHelpers\WebDavHelper;
/**
* Authentication functions
*/
class AuthContext implements Context {
private FeatureContext $featureContext;
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @param string $user
* @param string $password
*
* @return array
*/
public function createBasicAuthHeader(string $user, string $password): array {
$header = [];
$authString = \base64_encode("$user:$password");
$header["Authorization"] = "basic $authString";
return $header;
}
/**
* @param string $url
* @param string $method
* @param string|null $body
* @param array|null $headers
*
* @return ResponseInterface
*/
public function sendRequest(
string $url,
string $method,
?string $body = null,
?array $headers = []
): ResponseInterface {
// NOTE: preserving '/' for tests with special cases
// E.g: coreApiAuth/webDavSpecialURLs.feature
$url = \substr($url, 1);
$trimmedUrl = \ltrim($url, '/');
$slashCount = \strlen($url) - \strlen($trimmedUrl);
if (WebdavHelper::isDAVRequest($url)) {
$url = WebdavHelper::prefixRemotePhp($trimmedUrl);
}
$url = \str_repeat("/", $slashCount) . $url;
$fullUrl = $this->featureContext->getBaseUrl() . "/$url";
return HttpRequestHelper::sendRequest(
$fullUrl,
$this->featureContext->getStepLineRef(),
$method,
null,
null,
$headers,
$body,
);
}
/**
* @param string $user
* @param string $url
* @param string $method
* @param string|null $body
* @param array|null $headers
* @param string|null $property
*
* @return ResponseInterface
*/
public function requestUrlWithBasicAuth(
string $user,
string $url,
string $method,
?string $body = null,
?array $headers = null,
?string $property = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$url = $this->featureContext->substituteInLineCodes(
$url,
$user
);
$authHeader = $this->createBasicAuthHeader($user, $this->featureContext->getPasswordForUser($user));
$headers = \array_merge($headers ?? [], $authHeader);
if ($property !== null) {
$body = $this->featureContext->getBodyForOCSRequest($method, $property);
}
return $this->sendRequest(
$url,
$method,
$body,
$headers
);
}
/**
* @When a user requests :url with :method and no authentication
*
* @param string $url
* @param string $method
*
* @return void
*/
public function userRequestsURLWithNoAuth(string $url, string $method): void {
$this->featureContext->setResponse($this->sendRequest($url, $method));
}
/**
* @When a user requests these endpoints with :method with body :body and no authentication about user :user
*
* @param string $method
* @param string $body
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws JsonException
*/
public function userRequestsEndpointsWithBodyAndNoAuthThenStatusCodeAboutUser(
string $method,
string $body,
string $ofUser,
TableNode $table
): void {
$ofUser = \strtolower($this->featureContext->getActualUsername($ofUser));
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest($row['endpoint'], $method, $body);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When a user requests these endpoints with :method with no authentication about user :user
*
* @param string $method
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsEndpointsWithoutBodyAndNoAuthAboutUser(
string $method,
string $ofUser,
TableNode $table
): void {
$ofUser = \strtolower($this->featureContext->getActualUsername($ofUser));
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest($row['endpoint'], $method);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When a user requests these endpoints with :method and no authentication
*
* @param string $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsEndpointsWithNoAuthentication(string $method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$this->featureContext->setResponse(
$this->sendRequest(
$this->featureContext->substituteInLineCodes($row['endpoint']),
$method
)
);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When a user requests these URLs with :method and no authentication
*
* @param $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function aUserRequestsTheseUrlsWithAndNoAuthentication($method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['service']);
foreach ($table->getHash() as $row) {
$this->featureContext->setResponse(
HttpRequestHelper::sendRequest(
$this->featureContext->substituteInLineCodes($row['endpoint']),
$this->featureContext->getStepLineRef(),
$method
)
);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When the user :user requests these endpoints with :method with basic auth
*
* @param string $user
* @param string $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsEndpointsWithBasicAuth(string $user, string $method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$response = $this->requestUrlWithBasicAuth($user, $row['endpoint'], $method);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When /^user "([^"]*)" requests these endpoints with "([^"]*)" to (?:get|set) property "([^"]*)" about user "([^"]*)"$/
*
* @param string $user
* @param string $method
* @param string $property
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theUserRequestsTheseEndpointsToGetOrSetPropertyAboutUser(
string $user,
string $method,
string $property,
string $ofUser,
TableNode $table
): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->requestUrlWithBasicAuth($user, $row['endpoint'], $method, null, null, $property);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When the administrator requests these endpoints with :method
*
* @param string $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theAdminRequestsTheseEndpointsWithMethod(string $method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$response = $this->requestUrlWithBasicAuth(
$this->featureContext->getAdminUsername(),
$row['endpoint'],
$method
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests :url with :method using basic auth
*
* @param string $user
* @param string $url
* @param string $method
*
* @return void
*/
public function userRequestsURLUsingBasicAuth(
string $user,
string $url,
string $method
): void {
$response = $this->requestUrlWithBasicAuth($user, $url, $method);
$this->featureContext->setResponse($response);
}
/**
* @When user :user requests :url with :method using basic auth and with headers
*
* @param string $user
* @param string $url
* @param string $method
* @param TableNode $headersTable
*
* @return void
* @throws Exception
*/
public function userRequestsURLWithUsingBasicAuthAndDepthHeader(
string $user,
string $url,
string $method,
TableNode $headersTable
): void {
$user = $this->featureContext->getActualUsername($user);
$url = $this->featureContext->substituteInLineCodes(
$url,
$user
);
$this->featureContext->verifyTableNodeColumns(
$headersTable,
['header', 'value']
);
$headers = [];
foreach ($headersTable as $row) {
$headers[$row['header']] = $row ['value'];
}
$this->featureContext->setResponse(
$this->requestUrlWithBasicAuth(
$user,
$url,
$method,
null,
$headers
)
);
}
/**
* @When user :user requests these endpoints with :method using password :password
*
* @param string $user
* @param string $method
* @param string $password
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsWithPassword(
string $user,
string $method,
string $password,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
$authHeader = $this->createBasicAuthHeader($user, $this->featureContext->getActualPassword($password));
foreach ($table->getHash() as $row) {
$response = $this->sendRequest($row['endpoint'], $method, null, $authHeader);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method using password :password about user :ofUser
*
* @param string $user
* @param string $method
* @param string $password
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsUsingPasswordAboutUser(
string $user,
string $method,
string $password,
string $ofUser,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['destination']);
$headers = $this->createBasicAuthHeader($user, $this->featureContext->getActualPassword($password));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
if (isset($row['destination'])) {
$destination = $this->featureContext->substituteInLineCodes(
$row['destination'],
$ofUser
);
$headers['Destination'] = $this->featureContext->getBaseUrl()
. "/" . WebdavHelper::prefixRemotePhp(\ltrim($destination, "/"));
}
$response = $this->sendRequest(
$row['endpoint'],
$method,
null,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method including body :body using password :password about user :ofUser
*
* @param string $user
* @param string $method
* @param string $body
* @param string $password
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsWithBodyUsingPasswordAboutUser(
string $user,
string $method,
string $body,
string $password,
string $ofUser,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['destination']);
$headers = $this->createBasicAuthHeader($user, $this->featureContext->getActualPassword($password));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
if (isset($row['destination'])) {
$destination = $this->featureContext->substituteInLineCodes(
$row['destination'],
$ofUser
);
$headers['Destination'] = $this->featureContext->getBaseUrl()
. "/" . WebdavHelper::prefixRemotePhp(\ltrim($destination, "/"));
}
$response = $this->sendRequest(
$row['endpoint'],
$method,
$body,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method including body :body about user :ofUser
*
* @param string $user
* @param string $method
* @param string $body
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsIncludingBodyAboutUser(
string $user,
string $method,
string $body,
string $ofUser,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
$headers = [];
if ($method === 'MOVE' || $method === 'COPY') {
$headers['Destination'] = '/path/to/destination';
}
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->requestUrlWithBasicAuth(
$user,
$row['endpoint'],
$method,
$body,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :asUser requests these endpoints with :method using the password of user :ofUser
*
* @param string $asUser
* @param string $method
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsWithoutBodyUsingThePasswordOfUser(
string $asUser,
string $method,
string $ofUser,
TableNode $table
): void {
$asUser = $this->featureContext->getActualUsername($asUser);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
// do request as $asUser using password of $ofUser
$authHeader = $this->createBasicAuthHeader($asUser, $this->featureContext->getPasswordForUser($ofUser));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest(
$row['endpoint'],
$method,
null,
$authHeader
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :asUser requests these endpoints with :method including body :body using the password of user :user
*
* @param string $asUser
* @param string $method
* @param string|null $body
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws JsonException
*/
public function userRequestsTheseEndpointsIncludingBodyUsingPasswordOfUser(
string $asUser,
string $method,
?string $body,
string $ofUser,
TableNode $table
): void {
$asUser = $this->featureContext->getActualUsername($asUser);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
// do request as $asUser using password of $ofUser
$authHeader = $this->createBasicAuthHeader($asUser, $this->featureContext->getPasswordForUser($ofUser));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest(
$row['endpoint'],
$method,
$body,
$authHeader
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method about user :ofUser
*
* @param string $user
* @param string $method
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsAboutUser(
string $user,
string $method,
string $ofUser,
TableNode $table
): void {
$headers = [];
if ($method === 'MOVE' || $method === 'COPY') {
$baseUrl = $this->featureContext->getBaseUrl();
$suffix = $user;
if ($this->featureContext->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES) {
$suffix = $this->featureContext->getPersonalSpaceIdForUser($user);
}
$davPath = WebDavHelper::getDavPath($this->featureContext->getDavPathVersion(), $suffix);
$headers['Destination'] = "$baseUrl/$davPath/moved";
}
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->requestUrlWithBasicAuth(
$user,
$row['endpoint'],
$method,
null,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests :endpoint with :method without retrying
*
* @param string $user
* @param string $endpoint
* @param string $method
*
* @return void
*/
public function userRequestsURLWithoutRetry(
string $user,
string $endpoint,
string $method
): void {
$username = $this->featureContext->getActualUsername($user);
$endpoint = $this->featureContext->substituteInLineCodes(
$endpoint,
$username
);
$endpoint = \ltrim($endpoint, '/');
if (WebdavHelper::isDAVRequest($endpoint)) {
$endpoint = WebdavHelper::prefixRemotePhp($endpoint);
}
$fullUrl = $this->featureContext->getBaseUrl() . "/$endpoint";
$response = HttpRequestHelper::sendRequestOnce(
$fullUrl,
$this->featureContext->getStepLineRef(),
$method,
$username,
$this->featureContext->getPasswordForUser($user)
);
$this->featureContext->setResponse($response);
}
/**
* @When user :user should not be able to log in with wrong password :password
*
* @param string $user
* @param string $password
*
* @return void
*/
public function userShouldNotBeAbleToLogInWithWrongPassword(
string $user,
string $password
): void {
TokenHelper::clearUserTokens($user, $this->featureContext->getBaseUrl());
$response = TokenHelper::makeLoginRequest(
$user,
$password,
$this->featureContext->getBaseUrl(),
new \GuzzleHttp\Cookie\CookieJar()
);
// why is not 401 returned?
$this->featureContext->theHTTPStatusCodeShouldBe(204, 'should not be able to log in', $response);
}
}
@@ -0,0 +1,322 @@
<?php declare(strict_types=1);
/**
* @author Joas Schilling <coding@schilljs.com>
* @author Sergio Bertolin <sbertolin@owncloud.com>
* @author Phillip Davis <phil@jankaritech.com>
* @copyright Copyright (c) 2018, ownCloud GmbH
*
* 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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
use TestHelpers\OcsApiHelper;
use TestHelpers\BehatHelper;
use TestHelpers\HttpRequestHelper;
require_once 'bootstrap.php';
/**
* Capabilities context.
*/
class CapabilitiesContext implements Context {
private FeatureContext $featureContext;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
*
* @param string $username
* @param boolean $formatJson // if true then formats the response in json
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function userGetsCapabilities(string $username, ?bool $formatJson = false): ResponseInterface {
$user = $this->featureContext->getActualUsername($username);
$password = $this->featureContext->getPasswordForUser($user);
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user,
$password,
'GET',
'/cloud/capabilities' . ($formatJson ? '?format=json' : ''),
$this->featureContext->getStepLineRef(),
[],
$this->featureContext->getOcsApiVersion()
);
}
/**
* @return string
* @throws Exception|GuzzleException
*/
public function getAdminUsernameForCapabilitiesCheck(): string {
if (\TestHelpers\OcHelper::isTestingOnReva()) {
// When testing on reva we don't have a user called "admin" to use
// to access the capabilities. So create an ordinary user on-the-fly
// with a default password. That user should be able to get a
// capabilities response that the test can process.
$adminUsername = "PseudoAdminForRevaTest";
$createdUsers = $this->featureContext->getCreatedUsers();
if (!\array_key_exists($adminUsername, $createdUsers)) {
$this->featureContext->userHasBeenCreated(["userName" => $adminUsername]);
}
} else {
$adminUsername = $this->featureContext->getAdminUsername();
}
return $adminUsername;
}
/**
* @param SimpleXMLElement $xml of the capabilities
* @param string $capabilitiesApp the "app" name in the capabilities response
* @param string $capabilitiesPath the path to the element
*
* @return string
*/
public function getParameterValueFromXml(
SimpleXMLElement $xml,
string $capabilitiesApp,
string $capabilitiesPath
): string {
$path_to_element = \explode('@@@', $capabilitiesPath);
$answeredValue = $xml->{$capabilitiesApp};
foreach ($path_to_element as $element) {
$nameIndexParts = \explode('[', $element);
if (isset($nameIndexParts[1])) {
// This part of the path should be something like "some_element[1]"
// Separately extract the name and the index
$name = $nameIndexParts[0];
$index = (int) \explode(']', $nameIndexParts[1])[0];
// and use those to construct the reference into the next XML level
$answeredValue = $answeredValue->{$name}[$index];
} else {
if ($element !== "") {
$answeredValue = $answeredValue->{$element};
}
}
}
return (string) $answeredValue;
}
/**
* @When user :username retrieves the capabilities using the capabilities API
*
* @param string $username
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userRetrievesCapabilities(string $username): void {
$user = $this->featureContext->getActualUsername($username);
$this->featureContext->setResponse($this->userGetsCapabilities($user, true));
}
/**
* @When the administrator retrieves the capabilities using the capabilities API
*
* @return void
*/
public function theAdministratorGetsCapabilities(): void {
$user = $this->getAdminUsernameForCapabilitiesCheck();
$this->featureContext->setResponse($this->userGetsCapabilities($user, true));
}
/**
* @Then the major-minor-micro version data in the response should match the version string
*
* @return void
* @throws Exception
*/
public function checkVersionMajorMinorMicroResponse(): void {
$jsonResponse = $this->featureContext->getJsonDecodedResponseBodyContent();
$versionData = $jsonResponse->ocs->data->version;
$versionString = (string) $versionData->string;
// We expect that versionString will be in a format like "10.9.2 beta" or "10.9.2-alpha" or "10.9.2"
$result = \preg_match('/^[0-9]+\.[0-9]+\.[0-9]+/', $versionString, $matches);
Assert::assertSame(
1,
$result,
__METHOD__ . " version string '$versionString' does not start with a semver version"
);
// semVerParts should have an array with the 3 semver components of the version, e.g. "1", "9" and "2".
$semVerParts = \explode('.', $matches[0]);
$expectedMajor = $semVerParts[0];
$expectedMinor = $semVerParts[1];
$expectedMicro = $semVerParts[2];
$actualMajor = (string) $versionData->major;
$actualMinor = (string) $versionData->minor;
$actualMicro = (string) $versionData->micro;
Assert::assertSame(
$expectedMajor,
$actualMajor,
__METHOD__ . "'major' data item does not match with major version in string '$versionString'"
);
Assert::assertSame(
$expectedMinor,
$actualMinor,
__METHOD__ . "'minor' data item does not match with minor version in string '$versionString'"
);
Assert::assertSame(
$expectedMicro,
$actualMicro,
__METHOD__ . "'micro' data item does not match with micro (patch) version in string '$versionString'"
);
}
/**
* @Then the status.php response should include
*
* @param PyStringNode $jsonExpected
*
* @return void
* @throws Exception
*/
public function statusPhpRespondedShouldMatch(PyStringNode $jsonExpected): void {
$jsonExpectedDecoded = \json_decode($jsonExpected->getRaw(), true);
$jsonRespondedDecoded = $this->featureContext->getJsonDecodedResponse();
$response = $this->userGetsCapabilities($this->getAdminUsernameForCapabilitiesCheck());
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$responseXmlObject = HttpRequestHelper::getResponseXml($response, __METHOD__)->data->capabilities;
$edition = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@edition'
);
$product = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@product'
);
if (!\strlen($product)) {
Assert::fail(
"Cannot get product from core capabilities"
);
}
$productName = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@productname'
);
if (!\strlen($productName)) {
Assert::fail(
"Cannot get productname from core capabilities"
);
}
$jsonExpectedDecoded['edition'] = $edition;
$jsonExpectedDecoded['product'] = $product;
$jsonExpectedDecoded['productname'] = $productName;
// We are on КуСфера or reva or some other implementation. We cannot do "occ status".
// So get the expected version values by looking in the capabilities response.
$version = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@version'
);
if (!\strlen($version)) {
Assert::fail(
"Cannot get version from core capabilities"
);
}
$versionString = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@versionstring'
);
if (!\strlen($versionString)) {
Assert::fail(
"Cannot get versionstring from core capabilities"
);
}
$jsonExpectedDecoded['version'] = $version;
$jsonExpectedDecoded['versionstring'] = $versionString;
$errorMessage = "";
$errorFound = false;
foreach ($jsonExpectedDecoded as $key => $expectedValue) {
if (\array_key_exists($key, $jsonRespondedDecoded)) {
$actualValue = $jsonRespondedDecoded[$key];
if ($actualValue !== $expectedValue) {
$errorMessage .= "$key expected value was $expectedValue but actual value was $actualValue\n";
$errorFound = true;
}
} else {
$errorMessage .= "$key was not found in the status response\n";
$errorFound = true;
}
}
Assert::assertFalse($errorFound, $errorMessage);
// We have checked that the status.php response has data that matches up with
// data found in the capabilities response and/or the "occ status" command output.
// But the output might be reported wrongly in all of these in the same way.
// So check that the values also seem "reasonable".
$version = $jsonExpectedDecoded['version'];
$versionString = $jsonExpectedDecoded['versionstring'];
Assert::assertMatchesRegularExpression(
"/^\d+\.\d+\.\d+\.\d+$/",
$version,
"version should be in a form like 10.9.8.1 but is $version"
);
if (\preg_match("/^(\d+\.\d+\.\d+)\.\d+(-[0-9A-Za-z-]+)?(\+[0-9A-Za-z-]+)?$/", $version, $matches)) {
// We should have matched something like 10.9.8 - the first 3 numbers in the version.
// Ignore pre-releases and meta information
Assert::assertArrayHasKey(
1,
$matches,
"version $version could not match the pattern Major.Minor.Patch"
);
$majorMinorPatchVersion = $matches[1];
} else {
Assert::fail("version '$version' does not start in a form like 10.9.8");
}
Assert::assertStringStartsWith(
$majorMinorPatchVersion,
$versionString,
"version string should start with $majorMinorPatchVersion but is $versionString"
);
}
}
@@ -0,0 +1,517 @@
<?php declare(strict_types=1);
/**
* @author Roeland Jago Douma <rullzer@owncloud.com>
*
* @copyright Copyright (c) 2018, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
/**
* Checksum functions
*/
class ChecksumContext implements Context {
private FeatureContext $featureContext;
/**
* @param string $user
* @param string $source
* @param string $destination
* @param string $checksum
*
* @return ResponseInterface
*/
public function uploadFileToWithChecksumUsingTheAPI(
string $user,
string $source,
string $destination,
string $checksum
): ResponseInterface {
$file = \file_get_contents(
UploadHelper::getAcceptanceTestsDir() . $source
);
return $this->featureContext->makeDavRequest(
$user,
'PUT',
$destination,
['OC-Checksum' => $checksum],
$file
);
}
/**
* @When user :user uploads file :source to :destination with checksum :checksum using the WebDAV API
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $checksum
*
* @return void
*/
public function userUploadsFileToWithChecksumUsingTheAPI(
string $user,
string $source,
string $destination,
string $checksum
): void {
$this->featureContext->setResponse(
$this->uploadFileToWithChecksumUsingTheAPI($user, $source, $destination, $checksum)
);
}
/**
* @Given user :user has uploaded file :source to :destination with checksum :checksum
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $checksum
*
* @return void
*/
public function userHasUploadedFileToWithChecksumUsingTheAPI(
string $user,
string $source,
string $destination,
string $checksum
): void {
$user = $this->featureContext->getActualUsername($user);
$response = $this->uploadFileToWithChecksumUsingTheAPI(
$user,
$source,
$destination,
$checksum
);
$this->featureContext->theHTTPStatusCodeShouldBe([201,204], '', $response);
}
/**
* @param string $user
* @param string $content
* @param string $checksum
* @param string $destination
*
* @return ResponseInterface
*/
public function uploadFileWithContentAndChecksumToUsingTheAPI(
string $user,
string $content,
string $checksum,
string $destination
): ResponseInterface {
return $this->featureContext->makeDavRequest(
$user,
'PUT',
$destination,
['OC-Checksum' => $checksum],
$content
);
}
/**
* @Given user :user has uploaded file with content :content and checksum :checksum to :destination
*
* @param string $user
* @param string $content
* @param string $checksum
* @param string $destination
*
* @return void
*/
public function userHasUploadedFileWithContentAndChecksumToUsingTheAPI(
string $user,
string $content,
string $checksum,
string $destination
): void {
$user = $this->featureContext->getActualUsername($user);
$response = $this->uploadFileWithContentAndChecksumToUsingTheAPI($user, $content, $checksum, $destination);
$this->featureContext->theHTTPStatusCodeShouldBe(201, '', $response);
}
/**
* @When user :user requests the checksum of :path via propfind
*
* @param string $user
* @param string $path
*
* @return void
*/
public function userRequestsTheChecksumOfViaPropfind(string $user, string $path): void {
$this->featureContext->setResponse($this->propfindResourceChecksum($user, $path));
}
/**
* @param string $user
* @param string $path
* @param string|null $spaceId
*
* @return ResponseInterface
*/
public function propfindResourceChecksum(string $user, string $path, ?string $spaceId = null): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$body = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:checksums />
</d:prop>
</d:propfind>';
$password = $this->featureContext->getPasswordForUser($user);
return WebDavHelper::makeDavRequest(
$this->featureContext->getBaseUrl(),
$user,
$password,
'PROPFIND',
$path,
null,
$spaceId,
$this->featureContext->getStepLineRef(),
$body,
$this->featureContext->getDavPathVersion()
);
}
/**
* @Then the webdav checksum should match :expectedChecksum
*
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function theWebdavChecksumShouldMatch(string $expectedChecksum): void {
$bodyContents = $this->featureContext->getResponse()->getBody()->getContents();
$this->validateChecksum($bodyContents, $expectedChecksum);
}
/**
* @Then as user :user the webdav checksum of :path via propfind should match :expectedChecksum
*
* @param string $user
* @param string $path
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function asUserTheWebdavChecksumOfPathViaPropfindShouldMatch(
string $user,
string $path,
string $expectedChecksum
): void {
$user = $this->featureContext->getActualUsername($user);
$resource = $this->propfindResourceChecksum($user, $path);
$bodyContents = $resource->getBody()->getContents();
$this->validateChecksum($bodyContents, $expectedChecksum);
}
/**
* @param string $bodyContents
* @param string $expectedChecksum
*
* @return void
*/
public function validateChecksum(string $bodyContents, string $expectedChecksum): void {
$service = new Sabre\Xml\Service();
$parsed = $service->parse($bodyContents);
/*
* Fetch the checksum array
* The checksums are way down in the array:
* $checksums = $parsed[0]['value'][1]['value'][0]['value'][0];
* And inside is the actual checksum string:
* $checksums['value'][0]['value']
* The Asserts below check the existence of the expected key at every level
* of the nested array. This helps to see what happened if a test fails
* because the response structure is not as expected.
*/
Assert::assertIsArray(
$parsed,
__METHOD__
. " could not parse response as XML. Expected parsed XML to be an array but found " . $bodyContents
);
Assert::assertArrayHasKey(
0,
$parsed,
__METHOD__ . " parsed XML does not have key 0"
);
$parsed0 = $parsed[0];
Assert::assertArrayHasKey(
'value',
$parsed0,
__METHOD__ . " parsed XML parsed0 does not have key value"
);
$parsed0Value = $parsed0['value'];
Assert::assertArrayHasKey(
1,
$parsed0Value,
__METHOD__ . " parsed XML parsed0Value does not have key 1"
);
$parsed0Value1 = $parsed0Value[1];
Assert::assertArrayHasKey(
'value',
$parsed0Value1,
__METHOD__ . " parsed XML parsed0Value1 does not have key value after key 1"
);
$parsed0Value1Value = $parsed0Value1['value'];
Assert::assertArrayHasKey(
0,
$parsed0Value1Value,
__METHOD__ . " parsed XML parsed0Value1Value does not have key 0"
);
$parsed0Value1Value0 = $parsed0Value1Value[0];
Assert::assertArrayHasKey(
'value',
$parsed0Value1Value0,
__METHOD__ . " parsed XML parsed0Value1Value0 does not have key value"
);
$parsed0Value1Value0Value = $parsed0Value1Value0['value'];
Assert::assertArrayHasKey(
0,
$parsed0Value1Value0Value,
__METHOD__ . " parsed XML parsed0Value1Value0Value does not have key 0"
);
$checksums = $parsed0Value1Value0Value[0];
Assert::assertArrayHasKey(
'value',
$checksums,
__METHOD__ . " parsed XML checksums does not have key value"
);
$checksumsValue = $checksums['value'];
Assert::assertArrayHasKey(
0,
$checksumsValue,
__METHOD__ . " parsed XML checksumsValue does not have key 0"
);
$checksumsValue0 = $checksumsValue[0];
Assert::assertArrayHasKey(
'value',
$checksumsValue0,
__METHOD__ . " parsed XML checksumsValue0 does not have key value"
);
$actualChecksum = $checksumsValue0['value'];
Assert::assertEquals(
$expectedChecksum,
$actualChecksum,
"Expected: webDav checksum should be $expectedChecksum but got $actualChecksum"
);
}
/**
* @Then the header checksum should match :expectedChecksum
*
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function theHeaderChecksumShouldMatch(string $expectedChecksum): void {
$headerChecksums
= $this->featureContext->getResponse()->getHeader('OC-Checksum');
Assert::assertIsArray(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') did not return an array"
);
Assert::assertNotEmpty(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') returned an empty array. No checksum header was found."
);
$checksumCount = \count($headerChecksums);
Assert::assertTrue(
$checksumCount === 1,
__METHOD__ . " Expected 1 checksum in the header but found $checksumCount checksums"
);
$headerChecksum
= $headerChecksums[0];
Assert::assertEquals(
$expectedChecksum,
$headerChecksum,
"Expected: header checksum should match $expectedChecksum but got $headerChecksum"
);
}
/**
* @Then the header checksum when user :arg1 downloads file :arg2 using the WebDAV API should match :arg3
*
* @param string $user
* @param string $fileName
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function theHeaderChecksumWhenUserDownloadsFileUsingTheWebdavApiShouldMatch(
string $user,
string $fileName,
string $expectedChecksum
): void {
$response = $this->featureContext->downloadFileAsUserUsingPassword($user, $fileName);
$headerChecksums = $response->getHeader('OC-Checksum');
Assert::assertIsArray(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') did not return an array"
);
Assert::assertNotEmpty(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') returned an empty array. No checksum header was found."
);
$checksumCount = \count($headerChecksums);
Assert::assertTrue(
$checksumCount === 1,
__METHOD__ . " Expected 1 checksum in the header but found $checksumCount checksums"
);
$headerChecksum
= $headerChecksums[0];
Assert::assertEquals(
$expectedChecksum,
$headerChecksum,
"Expected: header checksum should match $expectedChecksum but got $headerChecksum"
);
}
/**
* @param string $user
* @param int $num
* @param int $total
* @param string $data
* @param string $destination
* @param string $expectedChecksum
*
* @return ResponseInterface
*/
public function uploadChunkFileOfWithToWithChecksum(
string $user,
int $num,
int $total,
string $data,
string $destination,
string $expectedChecksum
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$num -= 1;
$file = "$destination-chunking-42-$total-$num";
return $this->featureContext->makeDavRequest(
$user,
'PUT',
$file,
['OC-Checksum' => $expectedChecksum, 'OC-Chunked' => '1'],
$data
);
}
/**
* @When user :user uploads chunk file :num of :total with :data to :destination with checksum :expectedChecksum using the WebDAV API
*
* @param string $user
* @param int $num
* @param int $total
* @param string $data
* @param string $destination
* @param string $expectedChecksum
*
* @return void
*/
public function userUploadsChunkFileOfWithToWithChecksum(
string $user,
int $num,
int $total,
string $data,
string $destination,
string $expectedChecksum
): void {
$response = $this->uploadChunkFileOfWithToWithChecksum(
$user,
$num,
$total,
$data,
$destination,
$expectedChecksum
);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has uploaded chunk file :num of :total with :data to :destination with checksum :expectedChecksum
*
* @param string $user
* @param int $num
* @param int $total
* @param string $data
* @param string $destination
* @param string $expectedChecksum
*
* @return void
*/
public function userHasUploadedChunkFileOfWithToWithChecksum(
string $user,
int $num,
int $total,
string $data,
string $destination,
string $expectedChecksum
): void {
$response = $this->uploadChunkFileOfWithToWithChecksum(
$user,
$num,
$total,
$data,
$destination,
$expectedChecksum
);
$this->featureContext->theHTTPStatusCodeShouldBe(
[201, 206],
'',
$response
);
}
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
}
@@ -0,0 +1,915 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2024 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/>
*
*/
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use TestHelpers\CliHelper;
use TestHelpers\OcConfigHelper;
use TestHelpers\BehatHelper;
use Psr\Http\Message\ResponseInterface;
/**
* CLI context
*/
class CliContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* qsfera users storage path
*
* @return string
*/
public static function getUsersStoragePath(): string {
$path = getenv('OC_STORAGE_PATH') ?: '/var/lib/qsfera/storage/users';
// need for CI
$home = getenv('HOME');
$path = preg_replace('#^~/#', $home . '/', $path);
$path = str_replace('$HOME', $home, $path);
return rtrim($path, '/') . '/users';
}
/**
* qsfera project spaces storage path
*
* @return string
*/
public static function getProjectsStoragePath(): string {
$path = getenv('OC_STORAGE_PATH') ?: '/var/lib/qsfera/storage/users';
return $path . '/projects';
}
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* expects a file to exist at the given path
*
* @param string $path
* @param string|null $sizeGb
* @param int $maxSeconds
*
* @return void
*/
private function waitForPath(string $path, ?string $sizeGb = null, int $maxSeconds = 10): void {
$escapedPath = escapeshellarg($path);
for ($i = 0; $i < $maxSeconds * 5; $i++) {
if ($sizeGb) {
if (!preg_match('/^(\d+)gb$/i', $sizeGb, $matches)) {
throw new \InvalidArgumentException("Invalid size format: $sizeGb. Use formats like 1gb, 5gb.");
}
$targetBytes = (int)$matches[1] * 1024 * 1024 * 1024;
$body = [
"command" => "[ -f $escapedPath ] && stat -c%s $escapedPath || echo 0",
"raw" => true
];
$data = json_decode((string)CliHelper::runCommand($body)->getBody(), true);
if (isset($data['message']) && (int)trim($data['message']) >= $targetBytes) {
return;
}
} else {
$body = [
"command" => "ls $escapedPath >/dev/null 2>&1 && echo exists || echo not_exists",
"raw" => true
];
$response = CliHelper::runCommand($body);
$data = json_decode((string)$response->getBody(), true);
if (isset($data['message']) && trim($data['message']) === 'exists') {
return;
}
}
usleep(200000);
}
throw new \Exception("Timeout waiting for: $path");
}
/**
* @Given the administrator has stopped the server
*
* @return void
*/
public function theAdministratorHasStoppedTheServer(): void {
$response = OcConfigHelper::stopQsfera();
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @Given /^the administrator (?:starts|has started) the server$/
*
* @return void
*/
public function theAdministratorHasStartedTheServer(): void {
$response = OcConfigHelper::startQsfera();
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @When /^the administrator resets the password of (non-existing|existing) user "([^"]*)" to "([^"]*)" using the CLI$/
*
* @param string $status
* @param string $user
* @param string $password
*
* @return void
*/
public function theAdministratorResetsThePasswordOfUserUsingTheCLI(
string $status,
string $user,
string $password
): void {
$command = "idm resetpassword -u $user";
$body = [
"command" => $command,
"inputs" => [$password, $password]
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
if ($status === "non-existing") {
return;
}
$this->featureContext->updateUserPassword($user, $password);
}
/**
* @When the administrator deletes the empty trashbin folders using the CLI
*
* @return void
*/
public function theAdministratorDeletesEmptyTrashbinFoldersUsingTheCli(): void {
$path = $this->featureContext->getStorageUsersRoot();
$command = "trash purge-empty-dirs -p $path --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator checks the backup consistency using the CLI
*
* @return void
*/
public function theAdministratorChecksTheBackupConsistencyUsingTheCli(): void {
$path = $this->featureContext->getStorageUsersRoot();
$command = "backup consistency -p $path";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator creates app token for user :user with expiration time :expirationTime using the auth-app CLI
*
* @param string $user
* @param string $expirationTime
*
* @return void
*/
public function theAdministratorCreatesAppTokenForUserWithExpirationTimeUsingTheAuthAppCLI(
string $user,
string $expirationTime
): void {
$user = $this->featureContext->getActualUserName($user);
$command = "auth-app create --user-name=$user --expiration=$expirationTime";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @Given user :user has created app token with expiration time :expirationTime using the auth-app CLI
*
* @param string $user
* @param string $expirationTime
*
* @return void
*/
public function userHasCreatedAppTokenWithExpirationTimeUsingTheAuthAppCLI(
string $user,
string $expirationTime
): void {
$user = $this->featureContext->getActualUserName($user);
$command = "auth-app create --user-name=$user --expiration=$expirationTime";
$body = [
"command" => $command
];
$response = CliHelper::runCommand($body);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$jsonResponse = $this->featureContext->getJsonDecodedResponse($response);
Assert::assertSame("OK", $jsonResponse["status"]);
Assert::assertSame(
0,
$jsonResponse["exitCode"],
"Expected exit code to be 0, but got " . $jsonResponse["exitCode"]
);
}
/**
* @When the administrator removes all the file versions using the CLI
*
* @return void
*/
public function theAdministratorRemovesAllVersionsOfResources() {
$path = $this->featureContext->getStorageUsersRoot();
$command = "revisions purge -p $path --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator removes the versions of file :file of user :user from space :space using the CLI
*
* @param string $file
* @param string $user
* @param string $space
*
* @return void
*/
public function theAdministratorRemovesTheVersionsOfFileUsingFileId($file, $user, $space) {
$path = $this->featureContext->getStorageUsersRoot();
$fileId = $this->spacesContext->getFileId($user, $space, $file);
$command = "revisions purge -p $path -r $fileId --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When /^the administrator reindexes all spaces using the CLI$/
*
* @return void
*/
public function theAdministratorReindexesAllSpacesUsingTheCli(): void {
$endpoint = $this->featureContext->getBaseUrlHostName();
$command = "search index --all-spaces --endpoint $endpoint:9220 --insecure";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When /^the administrator reindexes a space "([^"]*)" using the CLI$/
*
* @param string $spaceName
*
* @return void
*/
public function theAdministratorReindexesASpaceUsingTheCli(string $spaceName): void {
$spaceId = $this->spacesContext->getSpaceIdByName($this->featureContext->getAdminUsername(), $spaceName);
$endpoint = $this->featureContext->getBaseUrlHostName();
$command = "search index --space $spaceId --endpoint $endpoint:9220 --insecure";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator removes the file versions of space :space using the CLI
*
* @param string $space
*
* @return void
*/
public function theAdministratorRemovesTheVersionsOfFilesInSpaceUsingSpaceId(string $space): void {
$path = $this->featureContext->getStorageUsersRoot();
$adminUsername = $this->featureContext->getAdminUsername();
$spaceId = $this->spacesContext->getSpaceIdByName($adminUsername, $space);
$command = "revisions purge -p $path -r $spaceId --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @Then the command should be successful
*
* @return void
*/
public function theCommandShouldBeSuccessful(): void {
$response = $this->featureContext->getResponse();
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$jsonResponse = $this->featureContext->getJsonDecodedResponse($response);
Assert::assertSame("OK", $jsonResponse["status"], $jsonResponse["message"]);
Assert::assertSame(
0,
$jsonResponse["exitCode"],
"Expected exit code to be 0, but got " . $jsonResponse["exitCode"]
. ". Message: " . $jsonResponse["message"]
);
}
/**
* @Then /^the command output (should|should not) contain "([^"]*)"$/
*
* @param string $shouldOrNot
* @param string $output
*
* @return void
*/
public function theCommandOutputShouldContain(string $shouldOrNot, string $output): void {
$response = $this->featureContext->getResponse();
$jsonResponse = $this->featureContext->getJsonDecodedResponse($response);
$output = $this->featureContext->substituteInLineCodes($output);
if ($shouldOrNot === "should") {
Assert::assertStringContainsString($output, $jsonResponse["message"]);
} else {
Assert::assertStringNotContainsString($output, $jsonResponse["message"]);
}
}
/**
* @When the administrator lists all the upload sessions
* @When the administrator lists all the upload sessions with flag :flag
*
* @param string|null $flag
*
* @return void
*/
public function theAdministratorListsAllTheUploadSessions(?string $flag = null): void {
if ($flag) {
$flag = "--$flag";
}
$command = "storage-users uploads sessions --json $flag";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator cleans upload sessions with the following flags:
*
* @param TableNode $table
*
* @return void
*/
public function theAdministratorCleansUploadSessionsWithTheFollowingFlags(TableNode $table): void {
$flag = "";
foreach ($table->getRows() as $row) {
$flag .= "--$row[0] ";
}
$flagString = trim($flag);
$command = "storage-users uploads sessions $flagString --clean --json";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator restarts the upload sessions that are in postprocessing
*
* @return void
*/
public function theAdministratorRestartsTheUploadSessionsThatAreInPostprocessing(): void {
$command = "storage-users uploads sessions --processing --restart --json";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator restarts the upload sessions of file :file
*
* @param string $file
*
* @return void
* @throws JsonException
*/
public function theAdministratorRestartsUploadSessionsOfFile(string $file): void {
$response = CliHelper::runCommand(["command" => "storage-users uploads sessions --json"]);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$responseArray = $this->getJSONDecodedCliMessage($response);
foreach ($responseArray as $item) {
if ($item->filename === $file) {
$uploadId = $item->id;
}
}
$command = "storage-users uploads sessions --id=$uploadId --restart --json";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @Then /^the CLI response (should|should not) contain these entries:$/
*
* @param string $shouldOrNot
* @param TableNode $table
*
* @return void
* @throws JsonException
*/
public function theCLIResponseShouldContainTheseEntries(string $shouldOrNot, TableNode $table): void {
$expectedFiles = $table->getColumn(0);
$responseArray = $this->getJSONDecodedCliMessage($this->featureContext->getResponse());
$resourceNames = [];
foreach ($responseArray as $item) {
if (isset($item->filename)) {
$resourceNames[] = $item->filename;
}
}
if ($shouldOrNot === "should not") {
foreach ($expectedFiles as $expectedFile) {
Assert::assertNotTrue(
\in_array($expectedFile, $resourceNames),
"The resource '$expectedFile' was found in the response."
);
}
} else {
foreach ($expectedFiles as $expectedFile) {
Assert::assertTrue(
\in_array($expectedFile, $resourceNames),
"The resource '$expectedFile' was not found in the response."
);
}
}
}
/**
* @param ResponseInterface $response
*
* @return array
* @throws JsonException
*/
public function getJSONDecodedCliMessage(ResponseInterface $response): array {
$responseBody = $this->featureContext->getJsonDecodedResponse($response);
// $responseBody["message"] contains a message info with the array of output json of the upload sessions command
// Example Output: "INFO memory is not limited, skipping package=github.com/KimMachineGun/automemlimit/memlimit [{<output-json>}]"
// So, only extracting the array of output json from the message
\preg_match('/(\[.*\])/', $responseBody["message"], $matches);
return \json_decode($matches[1], null, 512, JSON_THROW_ON_ERROR);
}
/**
* @AfterScenario @cli-uploads-sessions
*
* @return void
*/
public function cleanUploadsSessions(): void {
$command = "storage-users uploads sessions --clean";
$body = [
"command" => $command
];
$response = CliHelper::runCommand($body);
Assert::assertEquals("200", $response->getStatusCode(), "Failed to clean upload sessions");
}
/**
* @When the administrator creates the folder :folder for user :user on the POSIX filesystem
*
* @param string $folder
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFolder(string $folder, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$fullPath = "$storagePath/$userUuid/$folder";
$body = [
"command" => "mkdir -p $fullPath",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath($fullPath);
sleep(1);
}
/**
* @When the administrator lists the content of the POSIX storage folder of user :user
*
* @param string $user
*
* @return void
*/
public function theAdministratorCheckUsersFolder(string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "ls -la $storagePath/$userUuid",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator creates the file :file with content :content for user :user on the POSIX filesystem
*
* @param string $file
* @param string $content
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFile(string $file, string $content, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$fullPath = "$storagePath/$userUuid/$file";
$safeContent = escapeshellarg($content);
$body = [
"command" => "echo -n $safeContent > $fullPath",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath($fullPath);
sleep(1);
}
/**
* @When the administrator has created the file :file with content :content for user :user on the POSIX filesystem
*
* @param string $file
* @param string $content
* @param string $user
*
* @return void
*/
public function theAdministratorHasCreatedFile(string $file, string $content, string $user): void {
$this->theAdministratorCreatesFile($file, $content, $user);
$this->theCommandShouldBeSuccessful();
}
/**
* @When the administrator creates the file :file with size :size for user :user on the POSIX filesystem
*
* @param string $file
* @param string $size Example: "1gb", "5gb"
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesLargeFileWithSize(string $file, string $size, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$size = strtolower($size);
if (!preg_match('/^(\d+)gb$/', $size, $matches)) {
throw new \InvalidArgumentException("Invalid size format: $size. Use formats like 1gb, 5gb.");
}
$count = (int)$matches[1] * 1024; // 1GB = 1024M
$bs = '1M';
$filePath = "$storagePath/$userUuid/$file";
$body = [
"command" => "dd if=/dev/zero of=$filePath bs=$bs count=$count",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath($filePath, $size, 15);
sleep(7);
}
/**
* @When the administrator creates :count files sequentially in the directory :dir for user :user on the POSIX filesystem
*
* @param int $count
* @param string $dir
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFilesSequentially(int $count, string $dir, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath() . "/$userUuid/$dir";
$cmd = '';
for ($i = 1; $i <= $count; $i++) {
$cmd .= "echo -n \"file $i content\" > $storagePath/file_$i.txt; ";
}
$body = [
"command" => $cmd,
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath("$storagePath/file_$count.txt");
sleep(3);
}
/**
* @When the administrator creates :count files in parallel in the directory :dir for user :user on the POSIX filesystem
*
* @param int $count
* @param string $dir
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFilesInParallel(int $count, string $dir, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath() . "/$userUuid/$dir";
$cmd = "mkdir -p $storagePath; ";
for ($i = 1; $i <= $count; $i++) {
$cmd .= "echo -n \"parallel file $i content\" > $storagePath/parallel_$i.txt & ";
}
$cmd .= "wait";
$body = [
"command" => $cmd,
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath("$storagePath/parallel_$count.txt");
sleep(1);
}
/**
* @When the administrator puts the content :content into the file :file in the POSIX storage folder of user :user
*
* @param string $content
* @param string $file
* @param string $user
*
* @return void
*/
public function theAdministratorChangesFileContent(string $content, string $file, string $user): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$safeContent = escapeshellarg($content);
$body = [
"command" => "echo -n $safeContent >> $storagePath/$userUuid/$file",
"raw" => true
];
sleep(1);
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator reads the content of the file :file in the POSIX storage folder of user :user
*
* @param string $user
* @param string $file
*
* @return void
*/
public function theAdministratorReadsTheFileContent(string $user, string $file): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "cat $storagePath/$userUuid/$file",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator copies the file :file to the folder :folder for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $folder
*
* @return void
*/
public function theAdministratorCopiesFileToFolder(string $user, string $file, string $folder): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$source = "$storagePath/$userUuid/$file";
$destination = "$storagePath/$userUuid/$folder";
$body = [
"command" => "cp $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator renames the file :file to :newName for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $newName
*
* @return void
*/
public function theAdministratorRenamesFile(string $user, string $file, string $newName): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$source = "$storagePath/$userUuid/$file";
$destination = "$storagePath/$userUuid/$newName";
$body = [
"command" => "mv $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator moves the file :file to the folder :folder for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $folder
*
* @return void
*/
public function theAdministratorMovesFileToFolder(string $user, string $file, string $folder): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$source = "$storagePath/$userUuid/$file";
$destination = "$storagePath/$userUuid/$folder";
$body = [
"command" => "mv $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator deletes the file :file for user :user on the POSIX filesystem
*
* @param string $file
* @param string $user
*
* @return void
*/
public function theAdministratorDeletesFile(string $file, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "rm $storagePath/$userUuid/$file",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator deletes the folder :folder for user :user on the POSIX filesystem
*
* @param string $folder
* @param string $user
*
* @return void
*/
public function theAdministratorDeletesFolder(string $folder, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "rm -r $storagePath/$userUuid/$folder",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator copies the file :file to the space :space for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $space
*
* @return void
*/
public function theAdministratorCopiesFileToSpace(string $user, string $file, string $space): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$usersStoragePath = $this->getUsersStoragePath();
$projectsStoragePath = $this->getProjectsStoragePath();
$spaceId = $this->spacesContext->getSpaceIdByName($this->featureContext->getAdminUsername(), $space);
$spaceId = explode('$', $spaceId)[1];
$source = "$usersStoragePath/$userUuid/$file";
$destination = "$projectsStoragePath/$spaceId";
$body = [
"command" => "cp $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator deletes the project space :space on the POSIX filesystem
*
* @param string $space
*
* @return void
*/
public function theAdministratorDeletesSpace(string $space): void {
$projectsStoragePath = $this->getProjectsStoragePath();
$spaceId = $this->spacesContext->getSpaceIdByName($this->featureContext->getAdminUsername(), $space);
$spaceId = explode('$', $spaceId)[1];
$body = [
"command" => "rm -r $projectsStoragePath/$spaceId",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator checks the attribute :attribute of file :file for user :user on the POSIX filesystem
*
* @param string $attribute
* @param string $file
* @param string $user
*
* @return void
*/
public function theAdminChecksTheAttributeOfFileForUser(string $attribute, string $file, string $user): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "getfattr -n " . escapeshellarg($attribute) . " --only-values $storagePath/$userUuid/$file",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
}
@@ -0,0 +1,467 @@
<?php declare(strict_types=1);
/**
* @author Amrita Shrestha <amrita@jankaritech.com>
* @copyright Copyright (c) 2024 Amrita Shrestha <amrita@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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\CollaborationHelper;
use TestHelpers\BehatHelper;
/**
* steps needed to re-configure КуСфера server
*/
class CollaborationContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
private string $lastAppOpenData;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context from here
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $data
*
* @return void
*/
public function setLastAppOpenData(string $data): void {
$this->lastAppOpenData = $data;
}
/**
* @return string
*/
public function getLastAppOpenData(): string {
return $this->lastAppOpenData;
}
/**
* @When user :user checks the information of file :file of space :space using office :app
* @When user :user checks the information of file :file of space :space using office :app with view mode :view
*
* @param string $user
* @param string $file
* @param string $space
* @param string $app
* @param string|null $viewMode
*
* @return void
*
* @throws GuzzleException
*/
public function userChecksTheInformationOfFileOfSpaceUsingOffice(
string $user,
string $file,
string $space,
string $app,
string $viewMode = null
): void {
$fileId = $this->spacesContext->getFileId($user, $space, $file);
$response = \json_decode(
CollaborationHelper::sendPOSTRequestToAppOpen(
$fileId,
$app,
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$viewMode
)->getBody()->getContents()
);
$accessToken = $response->form_parameters->access_token;
// Extract the WOPISrc from the app_url
$parsedUrl = parse_url($response->app_url);
parse_str($parsedUrl['query'], $queryParams);
$wopiSrc = $queryParams['WOPISrc'];
$this->featureContext->setResponse(
HttpRequestHelper::get(
$wopiSrc . "?access_token=$accessToken",
$this->featureContext->getStepLineRef()
)
);
}
/**
* @When user :user creates a file :file inside folder :folder in space :space using wopi endpoint
* @When user :user tries to create a file :file inside folder :folder in space :space using wopi endpoint
*
* @param string $user
* @param string $file
* @param string $folder
* @param string $space
*
* @return void
* @throws GuzzleException
*/
public function userCreatesFileInsideFolderInSpaceUsingWopiEndpoint(
string $user,
string $file,
string $folder,
string $space
): void {
$parentContainerId = $this->spacesContext->getResourceId($user, $space, $folder);
$this->featureContext->setResponse(
CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$parentContainerId,
$file
)
);
}
/**
* @param string $file
* @param string $password
* @param string $folder
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public function createFile(string $file, string $password, string $folder = ""): void {
$token = $this->featureContext->shareNgGetLastCreatedLinkShareToken();
$baseUrl = $this->featureContext->getBaseUrl();
$davPath = WebDavHelper::getDavPath(WebDavHelper::DAV_VERSION_NEW, $token, "public-files");
$responseXmlObject = HttpRequestHelper::getResponseXml(
HttpRequestHelper::sendRequest(
"$baseUrl/$davPath/$folder",
$this->featureContext->getStepLineRef(),
"PROPFIND",
"public",
$this->featureContext->getActualPassword($password)
)
);
$xmlPart = $responseXmlObject->xpath("//d:prop/oc:fileid");
$parentContainerId = (string) $xmlPart[0];
$headers = [
"Public-Token" => $token
];
$this->featureContext->setResponse(
CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
"public",
$this->featureContext->getActualPassword($password),
$parentContainerId,
$file,
$headers
)
);
}
/**
* @When the public creates a file :file inside the last shared public link folder with password :password using wopi endpoint
* @When the public tries to create a file :file inside the last shared public link folder with password :password using wopi endpoint
*
* @param string $file
* @param string $password
*
* @return void
* @throws GuzzleException
*/
public function thePublicCreatesAFileInsideTheLastSharedPublicLinkFolderWithPasswordUsingWopiEndpoint(
string $file,
string $password
): void {
$this->createFile($file, $password);
}
/**
* @When the public creates a file :file inside folder :folder in the last shared public link space with password :password using wopi endpoint
* @When the public tries to create a file :file inside folder :folder in the last shared public link space with password :password using wopi endpoint
*
* @param string $file
* @param string $folder
* @param string $password
*
* @return void
* @throws GuzzleException
*/
public function thePublicCreatesAFileInsideFolderInTheLastSharedPublicLinkSpaceWithPasswordUsingWopiEndpoint(
string $file,
string $folder,
string $password
): void {
$this->createFile($file, $password, $folder);
}
/**
* @When user :user tries to check the information of file :file of space :space using office :app with invalid file-id
*
* @param string $user
* @param string $file
* @param string $space
* @param string $app
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userTriesToCheckTheInformationOfFileOfSpaceUsingOfficeWithInvalidFileId(
string $user,
string $file,
string $space,
string $app
): void {
$response = \json_decode(
CollaborationHelper::sendPOSTRequestToAppOpen(
$this->spacesContext->getFileId($user, $space, $file),
$app,
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef()
)->getBody()->getContents()
);
$accessToken = $response->form_parameters->access_token;
// Extract the WOPISrc from the app_url
$parsedUrl = parse_url($response->app_url);
parse_str($parsedUrl['query'], $queryParams);
$wopiSrc = $queryParams['WOPISrc'];
$position = strpos($wopiSrc, '/files/') + \strlen('/files/');
// Extract the base URL up to and including '/files/'
$fullUrl = substr($wopiSrc, 0, $position) . WebDavHelper::generateUUIDv4();
$this->featureContext->setResponse(
HttpRequestHelper::get(
$fullUrl . "?access_token=$accessToken",
$this->featureContext->getStepLineRef()
)
);
}
/**
* @When user :user tries to create a file :file inside deleted folder using wopi endpoint
*
* @param string $user
* @param string $file
*
* @return void
* @throws GuzzleException
*/
public function userTriesToCreateAFileInsideDeletedFolderUsingWopiEndpoint(string $user, string $file): void {
$parentContainerId = $this->featureContext->getStoredFileID();
$this->featureContext->setResponse(
CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$parentContainerId,
$file
)
);
}
/**
* @Given user :user has sent the following app-open request:
*
* @param string $user
* @param TableNode $properties
*
* @return void
* @throws GuzzleException
*/
public function userHasSentTheFollowingAppOpenRequest(string $user, TableNode $properties): void {
$rows = $properties->getRowsHash();
$appResponse = CollaborationHelper::sendPOSTRequestToAppOpen(
$this->spacesContext->getFileId($user, $rows['space'], $rows['resource']),
$rows['app'],
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef()
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $appResponse);
$this->setLastAppOpenData($appResponse->getBody()->getContents());
}
/**
* @When user :user tries to get the information of the last opened file using wopi endpoint
* @When user :user gets the information of the last opened file using wopi endpoint
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userGetsTheInformationOfTheLastOpenedFileUsingWopiEndpoint(string $user): void {
$response = json_decode($this->getLastAppOpenData());
$accessToken = $response->form_parameters->access_token;
// Extract the WOPISrc from the app_url
$parsedUrl = parse_url($response->app_url);
parse_str($parsedUrl['query'], $queryParams);
$wopiSrc = $queryParams['WOPISrc'];
$this->featureContext->setResponse(
HttpRequestHelper::get(
$wopiSrc . "?access_token=$accessToken",
$this->featureContext->getStepLineRef()
)
);
}
/**
* @Then /^the response (should|should not) contain the following MIME types:$/
*
* @param string $shouldOrNot
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theFollowingMimeTypesShouldExistForUser(string $shouldOrNot, TableNode $table): void {
$rows = $table->getRows();
$responseArray = $this->featureContext->getJsonDecodedResponse(
$this->featureContext->getResponse()
)['mime-types'];
$mimeTypes = \array_column($responseArray, 'mime_type');
foreach ($rows as $row) {
if ($shouldOrNot === "should not") {
Assert::assertFalse(
\in_array($row[0], $mimeTypes),
"the response should not contain the mimetype $row[0].\nMime Types found in response:\n"
. print_r($mimeTypes, true)
);
} else {
Assert::assertTrue(
\in_array($row[0], $mimeTypes),
"the response does not contain the mimetype $row[0].\nMime Types found in response:\n"
. print_r($mimeTypes, true)
);
}
}
}
/**
* @Then the app list response should contain the following template information for office :app:
*
* @param string $app
* @param TableNode $table
*
* @return void
*/
public function theAppListResponseShouldContainTheFollowingTemplateInformationForOffice(
string $app,
TableNode $table
): void {
$responseArray = $this->featureContext->getJsonDecodedResponse($this->featureContext->getResponse());
Assert::assertArrayHasKey(
"mime-types",
$responseArray,
"Expected 'mime-types' in the response but not found.\n" . print_r($responseArray, true)
);
$mimeTypes = $responseArray['mime-types'];
$mimeTypeMap = [];
foreach ($mimeTypes as $mimeType) {
$mimeTypeMap[$mimeType['mime_type']] = $mimeType;
}
foreach ($table->getColumnsHash() as $row) {
Assert::assertArrayHasKey(
$row['mime-type'],
$mimeTypeMap,
"Expected mime-type '{$row['mime-type']}' to exist in the response but it doesn't.\n"
. print_r($mimeTypeMap, true)
);
$mimeType = $mimeTypeMap[$row['mime-type']];
$found = false;
foreach ($mimeType['app_providers'] as $provider) {
if ($provider['name'] === $app && isset($row['target-extension'])) {
Assert::assertSame(
$row['target-extension'],
$provider['target_ext'],
"Expected 'target_ext' for $app to be '{$row['target-extension']}'"
. " but found '{$provider['target_ext']}'"
);
$found = true;
break;
}
}
if (!$found) {
Assert::fail(
"Expected response to contain app-provider '$app' with target-extension "
. "'{$row['target-extension']}' for mime-type '{$row['mime-type']}',"
. " but no matching provider was found.\n App Providers Found: "
. print_r($mimeType['app_providers'], true)
);
}
}
}
/**
* @When user :user has created a file :file using wopi endpoint
*
* @param string $user
* @param string $file
*
* @return string
* @throws GuzzleException
*/
public function userHasCreatedAFileInSpaceUsingWopiEndpoint(string $user, string $file): string {
$parentContainerId = $this->featureContext->getFileIdForPath($user, "/");
$response = CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$parentContainerId,
$file
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
$decodedResponse = $this->featureContext->getJsonDecodedResponseBodyContent($response);
return $decodedResponse->file_id;
}
}
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2018 Artur Neumann artur@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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* context containing favorites related API steps
*/
class FavoritesContext implements Context {
private WebDavPropertiesContext $webDavPropertiesContext;
/**
* @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should be favorited$/
*
* @param string $user
* @param string $path
* @param integer $expectedValue 0|1
* @param string|null $spaceId
*
* @return void
*/
public function asUserFileOrFolderShouldBeFavorited(
string $user,
string $path,
int $expectedValue = 1,
?string $spaceId = null
): void {
$property = "oc:favorite";
$this->webDavPropertiesContext->checkPropertyOfAFolder(
$user,
$path,
$property,
(string)$expectedValue,
null,
$spaceId,
);
}
/**
* @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should not be favorited$/
*
* @param string $user
* @param string $path
*
* @return void
*/
public function asUserFileShouldNotBeFavorited(string $user, string $path): void {
$this->asUserFileOrFolderShouldBeFavorited($user, $path, 0);
}
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->webDavPropertiesContext = BehatHelper::getContext(
$scope,
$environment,
'WebDavPropertiesContext'
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,551 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2018, ownCloud GmbH
*
* 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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Steps that relate to files_versions app
*/
class FilesVersionsContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @When user :user tries to get versions of file :file from :fileOwner
*
* @param string $user
* @param string $file
* @param string $fileOwner
*
* @return void
* @throws Exception
*/
public function userTriesToGetFileVersions(string $user, string $file, string $fileOwner): void {
$this->featureContext->setResponse($this->getFileVersions($user, $file, $fileOwner));
}
/**
* @When user :user gets the number of versions of file :file
*
* @param string $user
* @param string $file
*
* @return void
* @throws Exception
*/
public function userGetsFileVersions(string $user, string $file): void {
$this->featureContext->setResponse($this->getFileVersions($user, $file));
}
/**
* @When the public tries to get the number of versions of file :file with password :password using file-id :endpoint
*
* @param string $file
* @param string $password
* @param string $fileId
*
* @return void
*/
public function thePublicTriesToGetTheNumberOfVersionsOfFileWithPasswordUsingFileId(
string $file,
string $password,
string $fileId
): void {
$password = $this->featureContext->getActualPassword($password);
$this->featureContext->setResponse(
$this->featureContext->makeDavRequest(
"public",
"PROPFIND",
$fileId,
null,
null,
null,
"versions",
$this->featureContext->getDavPathVersion(),
false,
$password
)
);
}
/**
* @param string $user
* @param string $file
* @param string|null $fileOwner
* @param string|null $spaceId
*
* @return ResponseInterface
* @throws JsonException
* @throws GuzzleException
*/
public function getFileVersions(
string $user,
string $file,
?string $fileOwner = null,
?string $spaceId = null,
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileOwner = $fileOwner ? $this->featureContext->getActualUsername($fileOwner) : $user;
$fileId = $this->featureContext->getFileIdForPath($fileOwner, $file, $spaceId);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $file user $fileOwner not found (the file may not exist)"
);
return $this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
null,
$spaceId,
"versions"
);
}
/**
* @When user :user gets the number of versions of file :resource using file-id :fileId
* @When user :user tries to get the number of versions of file :resource using file-id :fileId
*
* @param string $user
* @param string $fileId
*
* @return void
*/
public function userGetsTheNumberOfVersionsOfFileOfTheSpace(string $user, string $fileId): void {
$this->featureContext->setResponse(
$this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
null,
null,
"versions"
)
);
}
/**
* @param string $user
* @param string $file
*
* @return ResponseInterface
*/
public function getFileVersionMetadata(string $user, string $file): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $file);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $file user $user not found (the file may not exist)"
);
$body = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:meta-version-edited-by />
<oc:meta-version-edited-by-name />
</d:prop>
</d:propfind>';
return $this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
$body,
null,
"versions",
);
}
/**
* @param string $user
* @param int $versionIndex
* @param string $path
*
* @return ResponseInterface
* @throws Exception
*/
public function restoreVersionIndexOfFile(string $user, int $versionIndex, string $path): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $path user $user not found (the file may not exist)"
);
$response = $this->listVersionFolder($user, $fileId, 1);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:response/d:href");
//restoring the version only works with DAV path v2
$destinationUrl = $this->featureContext->getBaseUrl() . "/" .
WebDavHelper::getDavPath(WebDavHelper::DAV_VERSION_NEW, $user) . \trim($path, "/");
$fullUrl = $this->featureContext->getBaseUrlWithoutPath() .
$xmlPart[$versionIndex];
return HttpRequestHelper::sendRequest(
$fullUrl,
$this->featureContext->getStepLineRef(),
'COPY',
$user,
$this->featureContext->getPasswordForUser($user),
['Destination' => $destinationUrl]
);
}
/**
* @Given user :user has restored version index :versionIndex of file :path
*
* @param string $user
* @param int $versionIndex
* @param string $path
*
* @return void
* @throws Exception
*/
public function userHasRestoredVersionIndexOfFile(string $user, int $versionIndex, string $path): void {
$response = $this->restoreVersionIndexOfFile($user, $versionIndex, $path);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When user :user restores version index :versionIndex of file :path using the WebDAV API
*
* @param string $user
* @param int $versionIndex
* @param string $path
*
* @return void
* @throws Exception
*/
public function userRestoresVersionIndexOfFile(string $user, int $versionIndex, string $path): void {
$response = $this->restoreVersionIndexOfFile($user, $versionIndex, $path);
$this->featureContext->setResponse($response, $user);
}
/**
* assert file versions count
*
* @param string $user
* @param string $fileId
* @param int $expectedCount
*
* @return void
* @throws Exception
*/
public function assertFileVersionsCount(string $user, string $fileId, int $expectedCount): void {
$response = $this->listVersionFolder($user, $fileId, 1);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$actualCount = \count($responseXmlObject->xpath("//d:prop/d:getetag")) - 1;
if ($actualCount === -1) {
$actualCount = 0;
}
Assert::assertEquals(
$expectedCount,
$actualCount,
"Expected $expectedCount versions but found $actualCount in \n" . $responseXmlObject->asXML()
);
}
/**
* @Then the version folder of file :path for user :user should contain :count element(s)
*
* @param string $path
* @param string $user
* @param int $count
*
* @return void
* @throws Exception
*/
public function theVersionFolderOfFileShouldContainElements(
string $path,
string $user,
int $count
): void {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path);
Assert::assertNotNull(
$fileId,
__METHOD__
. ". file '$path' for user '$user' not found (the file may not exist)"
);
$this->assertFileVersionsCount($user, $fileId, $count);
}
/**
* @Then the content length of file :path with version index :index for user :user in versions folder should be :length
*
* @param string $path
* @param int $index
* @param string $user
* @param int $length
*
* @return void
* @throws Exception
*/
public function theContentLengthOfFileForUserInVersionsFolderIs(
string $path,
int $index,
string $user,
int $length
): void {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $path user $user not found (the file may not exist)"
);
$response = $this->listVersionFolder($user, $fileId, 1, ['d:getcontentlength']);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:prop/d:getcontentlength");
Assert::assertEquals(
$length,
(int) $xmlPart[$index],
"The content length of file $path with version $index for user $user was
expected to be $length but the actual content length is $xmlPart[$index]"
);
}
/**
* @param string $user
* @param string $path
* @param string $index
* @param string|NullCpuCoreFinder $spaceId
*
* @return ResponseInterface
* @throws Exception
*/
public function downloadVersion(
string $user,
string $path,
string $index,
?string $spaceId = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path, $spaceId);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $path user $user not found (the file may not exist)"
);
$index = (int)$index;
$response = $this->listVersionFolder($user, $fileId, 1);
if ($response->getStatusCode() === 403) {
return $response;
}
$responseXmlObject = new SimpleXMLElement($response->getBody()->getContents());
$xmlPart = $responseXmlObject->xpath("//d:response/d:href");
if (!isset($xmlPart[$index])) {
Assert::fail(
'could not find version of path "' . $path . '" with index "' . $index . '"'
);
}
// the href already contains the path
$url = WebDavHelper::sanitizeUrl(
$this->featureContext->getBaseUrlWithoutPath() . $xmlPart[$index]
);
return HttpRequestHelper::get(
$url,
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
}
/**
* @When user :user downloads the version of file :path with the index :index
*
* @param string $user
* @param string $path
* @param string $index
*
* @return void
* @throws Exception
*/
public function userDownloadsVersion(string $user, string $path, string $index): void {
$this->featureContext->setResponse($this->downloadVersion($user, $path, $index), $user);
}
/**
* @Then /^the content of version index "([^"]*)" of file "([^"]*)" for user "([^"]*)" should be "([^"]*)"$/
*
* @param string $index
* @param string $path
* @param string $user
* @param string $content
*
* @return void
* @throws Exception
*/
public function theContentOfVersionIndexOfFileForUserShouldBe(
string $index,
string $path,
string $user,
string $content
): void {
$response = $this->downloadVersion($user, $path, $index);
$this->featureContext->theHTTPStatusCodeShouldBe("200", '', $response);
$this->featureContext->checkDownloadedContentMatches($content, '', $response);
}
/**
* @When /^user "([^"]*)" retrieves the meta information of (file|fileId) "([^"]*)" using the meta API$/
*
* @param string $user
* @param string $fileOrFileId
* @param string $path
*
* @return void
*/
public function userGetMetaInfo(string $user, string $fileOrFileId, string $path): void {
$user = $this->featureContext->getActualUsername($user);
$baseUrl = $this->featureContext->getBaseUrl();
$password = $this->featureContext->getPasswordForUser($user);
if ($fileOrFileId === "file") {
$fileId = $this->featureContext->getFileIdForPath($user, $path);
$metaPath = "/meta/$fileId/";
} else {
$metaPath = "/meta/$path/";
}
$body = '<?xml version="1.0" encoding="utf-8"?>
<a:propfind xmlns:a="DAV:" xmlns:oc="http://owncloud.org/ns">
<a:prop>
<oc:meta-path-for-user />
</a:prop>
</a:propfind>';
$response = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$metaPath,
['Content-Type' => 'text/xml','Depth' => '0'],
null,
$this->featureContext->getStepLineRef(),
$body,
$this->featureContext->getDavPathVersion(),
null
);
$this->featureContext->setResponse($response);
}
/**
* returns the result parsed into a SimpleXMLElement
* with a registered namespace with 'd' as prefix and 'DAV:' as namespace
*
* @param string $user
* @param string $fileId
* @param int $folderDepth
* @param string[]|null $properties
*
* @return ResponseInterface
* @throws Exception
*/
public function listVersionFolder(
string $user,
string $fileId,
int $folderDepth,
?array $properties = null
): ResponseInterface {
if (!$properties) {
$properties = [
'd:getetag'
];
}
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getPasswordForUser($user);
$response = WebDavHelper::propfind(
$this->featureContext->getBaseUrl(),
$user,
$password,
$fileId,
$properties,
$this->featureContext->getStepLineRef(),
(string) $folderDepth,
null,
"versions"
);
return $response;
}
/**
* @When user :user gets the number of versions of file :file from space :space
* @When user :user tries to get the number of versions of file :file from space :space
*
* @param string $user
* @param string $file
* @param string $space
*
* @return void
*/
public function userGetsTheNumberOfVersionsOfFileFromSpace(string $user, string $file, string $space): void {
$fileId = $this->spacesContext->getFileId($user, $space, $file);
$this->featureContext->setResponse(
$this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
null,
null,
"versions"
)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,763 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <vscharf@owncloud.com>
* @copyright Copyright (c) 2023 Viktor Scharf vscharf@owncloud.com
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Behat\Gherkin\Node\PyStringNode;
use PHPUnit\Framework\Assert;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\EmailHelper;
use TestHelpers\OcsApiHelper;
use TestHelpers\GraphHelper;
use TestHelpers\SettingsHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Defines application features from the specific context.
*/
class NotificationContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
private string $notificationEndpointPath = '/apps/notifications/api/v1/notifications?format=json';
private string $globalNotificationEndpointPath = '/apps/notifications/api/v1/notifications/global';
private array $notificationIds;
/**
* @return array[]
*/
public function getNotificationIds(): array {
return $this->notificationIds;
}
/**
* @return array[]
*/
public function getLastNotificationId(): array {
return \end($this->notificationIds);
}
/**
* @AfterScenario
*
* @return void
*/
public function deleteDeprovisioningNotification(): void {
$payload["ids"] = ["deprovision"];
OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$this->featureContext->getAdminUsername(),
$this->featureContext->getAdminPassword(),
'DELETE',
$this->globalNotificationEndpointPath,
$this->featureContext->getStepLineRef(),
json_encode($payload)
);
}
/**
* @var string
*/
private string $userRecipient;
/**
* @param string $userRecipient
*
* @return void
*/
public function setUserRecipient(string $userRecipient): void {
$this->userRecipient = $userRecipient;
}
/**
* @return string
*/
public function getUserRecipient(): string {
return $this->userRecipient;
}
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
* @throws Exception
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $user
*
* @return ResponseInterface
*/
public function listAllNotifications(string $user): ResponseInterface {
$this->setUserRecipient($user);
$language = SettingsHelper::getLanguageSettingValue(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef()
);
$headers = ["accept-language" => $language];
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
'GET',
$this->notificationEndpointPath,
$this->featureContext->getStepLineRef(),
[],
2,
$headers
);
}
/**
* @When /^user "([^"]*)" lists all notifications$/
*
* @param string $user
*
* @return void
*/
public function userListAllNotifications(string $user): void {
$response = $this->listAllNotifications($user);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastHttpStatusCodesArray();
}
/**
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function deleteAllNotifications(string $user): ResponseInterface {
$response = $this->listAllNotifications($user);
if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
foreach ($responseBody as $value) {
// set notificationId
$this->notificationIds[] = $value->notification_id;
}
}
return $this->userDeletesNotification($user);
}
/**
* @When user :user deletes all notifications
*
* @param string $user
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userDeletesAllNotifications(string $user): void {
$response = $this->deleteAllNotifications($user);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has deleted all notifications
*
* @param string $user
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userHasDeletedAllNotifications(string $user): void {
$response = $this->deleteAllNotifications($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
}
/**
* @When user :user deletes a notification related to resource :resource with subject :subject
*
* @param string $user
* @param string $resource
* @param string $subject
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userDeletesNotificationOfResourceAndSubject(string $user, string $resource, string $subject): void {
$response = $this->listAllNotifications($user);
$this->filterNotificationsBySubjectAndResource($subject, $resource, $response);
$this->featureContext->setResponse($this->userDeletesNotification($user));
}
/**
* deletes notification
*
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function userDeletesNotification(string $user): ResponseInterface {
$this->setUserRecipient($user);
$payload["ids"] = $this->getNotificationIds();
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
'DELETE',
$this->notificationEndpointPath,
$this->featureContext->getStepLineRef(),
\json_encode($payload),
2
);
}
/**
* @Then the notifications should be empty
*
* @return void
* @throws Exception
*/
public function theNotificationsShouldBeEmpty(): void {
$statusCode = $this->featureContext->getResponse()->getStatusCode();
if ($statusCode !== 200) {
$response = $this->featureContext->getResponse()->getBody()->getContents();
throw new \Exception(
__METHOD__
. " Failed to get user notification list" . $response
);
}
$notifications = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
Assert::assertNull($notifications, "response should not contain any notification");
}
/**
* @Then user :user should not have any notification
*
* @param $user
*
* @return void
* @throws Exception
*/
public function userShouldNotHaveAnyNotification($user): void {
$response = $this->listAllNotifications($user);
$notifications = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
Assert::assertNull($notifications, "response should not contain any notification");
}
/**
* @Then /^there should be "([^"]*)" notifications$/
*
* @param int $numberOfNotification
*
* @return void
* @throws Exception
*/
public function userShouldHaveNotifications(int $numberOfNotification): void {
if (!isset($this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data)) {
throw new Exception("Notification is empty");
}
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
$actualNumber = \count($responseBody);
Assert::assertEquals(
$numberOfNotification,
$actualNumber,
"Expected number of notifications was '$numberOfNotification', but got '$actualNumber'"
);
}
/**
* @Then /^the JSON response should contain a notification message with the subject "([^"]*)" and the message-details should match$/
*
* @param string $subject
* @param PyStringNode $schemaString
*
* @return void
* @throws Exception
*/
public function theJsonDataFromLastResponseShouldMatch(
string $subject,
PyStringNode $schemaString
): void {
$responseBody = $this->filterResponseAccordingToNotificationSubject($subject);
// substitute the value here
$schemaString = $schemaString->getRaw();
$schemaString = $this->featureContext->substituteInLineCodes(
$schemaString,
$this->featureContext->getCurrentUser(),
[],
[],
null,
$this->getUserRecipient(),
);
$this->featureContext->assertJsonDocumentMatchesSchema(
$responseBody,
$this->featureContext->getJSONSchema($schemaString)
);
}
/**
* filter notification according to subject
*
* @param string $subject
* @param ResponseInterface|null $response
*
* @return object
*/
public function filterResponseAccordingToNotificationSubject(
string $subject,
?ResponseInterface $response = null
): object {
$response = $response ?? $this->featureContext->getResponse();
if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
foreach ($responseBody as $value) {
if (isset($value->subject) && $value->subject === $subject) {
$responseBody = $value;
// set notificationId
$this->notificationIds[] = $value->notification_id;
break;
}
}
} else {
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response);
}
return $responseBody;
}
/**
* filter notification according to subject and resource
*
* @param string $subject
* @param string $resource
* @param ResponseInterface|null $response
*
* @return array
*/
public function filterNotificationsBySubjectAndResource(
string $subject,
string $resource,
?ResponseInterface $response = null
): array {
$filteredNotifications = [];
$response = $response ?? $this->featureContext->getResponse();
$responseObject = $this->featureContext->getJsonDecodedResponseBodyContent($response);
if (!isset($responseObject->ocs->data)) {
Assert::fail("Response doesn't contain notification: " . print_r($responseObject, true));
}
$notifications = $responseObject->ocs->data;
foreach ($notifications as $notification) {
if (isset($notification->subject) && $notification->subject === $subject
&& isset($notification->messageRichParameters->resource->name)
&& $notification->messageRichParameters->resource->name === $resource
) {
$this->notificationIds[] = $notification->notification_id;
$filteredNotifications[] = $notification;
}
}
return $filteredNotifications;
}
/**
* @Then /^user "([^"]*)" should get a notification with subject "([^"]*)" and message:$/
*
* @param string $user
* @param string $subject
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userShouldGetANotificationWithMessage(string $user, string $subject, TableNode $table): void {
$count = 0;
// Sometimes the test might try to get the notifications before the server has created the notification.
// To prevent the test from failing because of that, try to list the notifications again
do {
if ($count > 0) {
\sleep(1);
}
$this->featureContext->setResponse(null);
$response = $this->listAllNotifications($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
++$count;
} while (!isset($this->filterResponseAccordingToNotificationSubject($subject, $response)->message)
&& $count <= 10
);
if (isset($this->filterResponseAccordingToNotificationSubject($subject, $response)->message)) {
$actualMessage = str_replace(
["\r", "\n"],
" ",
$this->filterResponseAccordingToNotificationSubject($subject, $response)->message
);
} else {
throw new \Exception("Notification was not found even after retrying for 5 seconds.");
}
$expectedMessage = $table->getColumnsHash()[0]['message'];
Assert::assertStringStartsWith(
$expectedMessage,
$actualMessage,
__METHOD__ . "expected message to start with '$expectedMessage' but found'$actualMessage'"
);
}
/**
* @Then user :user should get a notification for resource :resource with subject :subject and message:
*
* @param string $user
* @param string $resource
* @param string $subject
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userShouldGetNotificationForResourceWithMessage(
string $user,
string $resource,
string $subject,
TableNode $table
): void {
$response = $this->listAllNotifications($user);
$notification = $this->filterNotificationsBySubjectAndResource($subject, $resource, $response);
if (\count($notification) === 1) {
$actualMessage = str_replace(["\r", "\r"], " ", $notification[0]->message);
$expectedMessage = $table->getColumnsHash()[0]['message'];
Assert::assertStringStartsWith(
$expectedMessage,
$actualMessage,
__METHOD__ . "expected message to start with '$expectedMessage' but found'$actualMessage'"
);
$response = $this->userDeletesNotification($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
} elseif (\count($notification) === 0) {
throw new \Exception(
"Response doesn't contain any notification with resource '$resource' and subject '$subject'.\n"
. print_r($notification, true)
);
} else {
throw new \Exception(
"Response contains more than one notification with resource '$resource' and subject '$subject'.\n"
. print_r($notification, true)
);
}
}
/**
* @Then user :user should not get a notification related to resource :resource with subject :subject
*
* @param string $user
* @param string $resource
* @param string $subject
*
* @return void
*/
public function userShouldNotHaveANotificationRelatedToResourceWithSubject(
string $user,
string $resource,
string $subject
): void {
$response = $this->listAllNotifications($user);
$filteredResponse = $this->filterNotificationsBySubjectAndResource($subject, $resource, $response);
Assert::assertCount(
0,
$filteredResponse,
"Response should not contain notification related to resource '$resource' with subject '$subject' but found"
. print_r($filteredResponse, true)
);
}
/**
* @Then user :user should have received the following email from user :sender about the share of project space :spaceName
*
* @param string $user
* @param string $sender
* @param string $spaceName
* @param PyStringNode $content
*
* @return void
* @throws Exception
*/
public function userShouldHaveReceivedTheFollowingEmailFromUserAboutTheShareOfProjectSpace(
string $user,
string $sender,
string $spaceName,
PyStringNode $content
): void {
$rawExpectedEmailBodyContent = \str_replace("\r\n", "\n", $content->getRaw());
$this->featureContext->setResponse(
GraphHelper::getMySpaces(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
'',
$this->featureContext->getStepLineRef()
)
);
$expectedEmailBodyContent = $this->featureContext->substituteInLineCodes(
$rawExpectedEmailBodyContent,
$sender,
[],
[
[
"code" => "%space_id%",
"function" =>
[$this->spacesContext, "getSpaceIdByName"],
"parameter" => [$sender, $spaceName]
],
]
);
$this->assertEmailContains($user, $expectedEmailBodyContent);
}
/**
* @Then user :user should have received the following email from user :sender
*
* @param string $user
* @param string $sender
* @param PyStringNode $content
*
* @return void
* @throws Exception
*/
public function userShouldHaveReceivedTheFollowingEmailFromUser(
string $user,
string $sender,
PyStringNode $content
): void {
$rawExpectedEmailBodyContent = \str_replace("\r\n", "\n", $content->getRaw());
$expectedEmailBodyContent = $this->featureContext->substituteInLineCodes(
$rawExpectedEmailBodyContent,
$sender
);
$this->assertEmailContains($user, $expectedEmailBodyContent);
}
/**
* @Then user :user should have received the following email from user :sender ignoring whitespaces
*
* @param string $user
* @param string $sender
* @param PyStringNode $content
*
* @return void
* @throws Exception
*/
public function userShouldHaveReceivedTheFollowingEmailFromUserIgnoringWhitespaces(
string $user,
string $sender,
PyStringNode $content
): void {
$rawExpectedEmailBodyContent = \str_replace("\r\n", "\n", $content->getRaw());
$expectedEmailBodyContent = $this->featureContext->substituteInLineCodes(
$rawExpectedEmailBodyContent,
$sender
);
$this->assertEmailContains($user, $expectedEmailBodyContent, true);
}
/***
* @param string $user
* @param string $expectedEmailBodyContent
* @param bool $ignoreWhiteSpace
*
* @return void
* @throws GuzzleException
*/
public function assertEmailContains(
string $user,
string $expectedEmailBodyContent,
$ignoreWhiteSpace = false
): void {
$address = $this->featureContext->getEmailAddressForUser($user);
$this->featureContext->pushEmailRecipientAsMailBox($address);
// assert with retries as email delivery might be delayed
$retried = 0;
do {
$actualEmailBodyContent = EmailHelper::getBodyOfLastEmail(
$address,
$this->featureContext->getStepLineRef()
);
if ($ignoreWhiteSpace) {
$expectedEmailBodyContent = preg_replace('/\s+/', '', $expectedEmailBodyContent);
$actualEmailBodyContent = preg_replace('/\s+/', '', $actualEmailBodyContent);
}
$tryAgain = !\str_contains($actualEmailBodyContent, $expectedEmailBodyContent)
&& $retried <= STANDARD_RETRY_COUNT;
$retried++;
if ($tryAgain) {
$mailBox = EmailHelper::getMailBoxFromEmail($address);
echo "[INFO] Checking last email content for '$mailBox'. (Retry $retried)\n";
// wait for 1 second before trying again
sleep(1);
}
} while ($tryAgain);
Assert::assertStringContainsString(
$expectedEmailBodyContent,
$actualEmailBodyContent,
"The email address '$address' should have received an"
. " email with the body containing '$expectedEmailBodyContent'"
. " but the received email is '$actualEmailBodyContent'"
);
}
/**
* Delete all emails from the mailboxes
*
* @AfterScenario @email
*
* @return void
*/
public function clearMailboxes(): void {
$users = \array_keys($this->featureContext->getCreatedUsers());
try {
if (!empty($users)) {
foreach ($users as $emailRecipient) {
$retried = 0;
do {
$res = EmailHelper::deleteAllEmails(
EmailHelper::getLocalEmailUrl(),
$emailRecipient,
$this->featureContext->getStepLineRef(),
);
$deleteStatus = $res->getStatusCode();
$mailBox = EmailHelper::getMailboxInformation($emailRecipient);
$tryAgain = ($deleteStatus !== 200 || !empty($mailBox)) && $retried <= STANDARD_RETRY_COUNT;
$retried++;
if ($tryAgain) {
echo "[INFO] Clearing mailbox '$emailRecipient'."
. " Status: $deleteStatus. Emails: " . \count($mailBox) . "."
. " (Retry $retried)\n";
// wait for 1 second before trying again
sleep(1);
}
} while ($tryAgain);
}
}
} catch (Exception $e) {
echo __METHOD__ .
" could not delete inbucket messages, is inbucket set up?\n" .
$e->getMessage();
}
}
/**
*
* @param string|null $user
* @param string|null $deprovision_date
* @param string|null $deprovision_date_format
*
* @return ResponseInterface
*
* @throws GuzzleException
*
* @throws JsonException
*/
public function userCreatesDeprovisioningNotification(
?string $user = null,
?string $deprovision_date = "2043-07-04T11:23:12Z",
?string $deprovision_date_format= "2006-01-02T15:04:05Z07:00"
): ResponseInterface {
$payload["type"] = "deprovision";
$payload["data"] = [
"deprovision_date" => $deprovision_date, "deprovision_date_format" => $deprovision_date_format];
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user ? $this->featureContext->getActualUsername($user) : $this->featureContext->getAdminUsername(),
$user ? $this->featureContext->getPasswordForUser($user) : $this->featureContext->getAdminPassword(),
'POST',
$this->globalNotificationEndpointPath,
$this->featureContext->getStepLineRef(),
json_encode($payload)
);
}
/**
* @When the administrator creates a deprovisioning notification
* @When user :user tries to create a deprovisioning notification
*
* @param string|null $user
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function theAdministratorCreatesADeprovisioningNotification(?string $user = null) {
$response = $this->userCreatesDeprovisioningNotification($user);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastHttpStatusCodesArray();
}
/**
* @When the administrator creates a deprovisioning notification for date :deprovision_date of format :deprovision_date_format
*
* @param $deprovision_date
* @param $deprovision_date_format
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function theAdministratorCreatesADeprovisioningNotificationUsingDateFormat(
$deprovision_date,
$deprovision_date_format
) {
$response = $this->userCreatesDeprovisioningNotification(null, $deprovision_date, $deprovision_date_format);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastHttpStatusCodesArray();
}
/**
* @Given the administrator has created a deprovisioning notification
*
* @return void
*/
public function userHasCreatedDeprovisioningNotification(): void {
$response = $this->userCreatesDeprovisioningNotification();
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
}
/**
* @When the administrator deletes the deprovisioning notification
* @When user :user tries to delete the deprovisioning notification
*
* @param string|null $user
*
* @return void
*/
public function userDeletesDeprovisioningNotification(?string $user = null): void {
$payload["ids"] = ["deprovision"];
$response = OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user ? $this->featureContext->getActualUsername($user) : $this->featureContext->getAdminUsername(),
$user ? $this->featureContext->getPasswordForUser($user) : $this->featureContext->getAdminPassword(),
'DELETE',
$this->globalNotificationEndpointPath,
$this->featureContext->getStepLineRef(),
json_encode($payload)
);
$this->featureContext->setResponse($response);
}
}
@@ -0,0 +1,438 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2019, ownCloud GmbH
*
* 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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
use TestHelpers\HttpRequestHelper;
use TestHelpers\OcsApiHelper;
use TestHelpers\TranslationHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* steps needed to send requests to the OCS API
*/
class OCSContext implements Context {
private FeatureContext $featureContext;
/**
* @When /^the user sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/
*
* @param string $verb
* @param string $url
*
* @return void
*/
public function theUserSendsToOcsApiEndpoint(string $verb, string $url): void {
$response = $this->theUserSendsToOcsApiEndpointWithBody($verb, $url);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/
* @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" using password "([^"]*)"$/
*
* @param string $user
* @param string $verb
* @param string $url
* @param string|null $password
*
* @return void
*/
public function userSendsToOcsApiEndpoint(string $user, string $verb, string $url, ?string $password = null): void {
$response = $this->sendRequestToOcsEndpoint(
$user,
$verb,
$url,
null,
$password
);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
* @param string $verb
* @param string $url
* @param TableNode|null $body
* @param string|null $password
* @param array|null $headers
*
* @return ResponseInterface
*/
public function sendRequestToOcsEndpoint(
string $user,
string $verb,
string $url,
?TableNode $body = null,
?string $password = null,
?array $headers = null
): ResponseInterface {
/**
* array of the data to be sent in the body.
* contains $body data converted to an array
*/
$bodyArray = [];
if ($body instanceof TableNode) {
$bodyArray = $body->getRowsHash();
}
if ($user !== 'UNAUTHORIZED_USER') {
if ($password === null) {
$password = $this->featureContext->getPasswordForUser($user);
}
$user = $this->featureContext->getActualUsername($user);
} else {
$user = null;
$password = null;
}
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user,
$password,
$verb,
$url,
$this->featureContext->getStepLineRef(),
$bodyArray,
$this->featureContext->getOcsApiVersion(),
$headers
);
}
/**
* @param string $verb
* @param string $url
* @param TableNode|null $body
*
* @return ResponseInterface
*/
public function adminSendsHttpMethodToOcsApiEndpointWithBody(
string $verb,
string $url,
?TableNode $body
): ResponseInterface {
$admin = $this->featureContext->getAdminUsername();
return $this->sendRequestToOcsEndpoint(
$admin,
$verb,
$url,
$body
);
}
/**
* @param string $verb
* @param string $url
* @param TableNode|null $body
*
* @return ResponseInterface
*/
public function theUserSendsToOcsApiEndpointWithBody(
string $verb,
string $url,
?TableNode $body = null
): ResponseInterface {
return $this->sendRequestToOcsEndpoint(
$this->featureContext->getCurrentUser(),
$verb,
$url,
$body
);
}
/**
* @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers$/
*
* @param string $user
* @param string $verb
* @param string $url
* @param TableNode $headersTable
*
* @return void
* @throws Exception
*/
public function userSendsToOcsApiEndpointWithHeaders(
string $user,
string $verb,
string $url,
TableNode $headersTable
): void {
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getPasswordForUser($user);
$this->featureContext->setResponse(
$this->sendRequestToOcsEndpoint(
$user,
$verb,
$url,
null,
$password,
$headersTable->getRowsHash()
)
);
}
/**
* @Then /^the OCS status code should be "([^"]*)"$/
*
* @param string $statusCode
* @param string $message
* @param ResponseInterface|null $response
*
* @return void
* @throws Exception
*/
public function theOCSStatusCodeShouldBe(
string $statusCode,
string $message = "",
?ResponseInterface $response = null
): void {
$statusCodes = explode(",", $statusCode);
$response = $response ?? $this->featureContext->getResponse();
$responseStatusCode = $this->getOCSResponseStatusCode(
$response
);
if (\is_array($statusCodes)) {
if ($message === "") {
$message = "OCS status code is not any of the expected values "
. \implode(",", $statusCodes) . " got " . $responseStatusCode;
}
Assert::assertContainsEquals(
$responseStatusCode,
$statusCodes,
$message
);
$this->featureContext->emptyLastOCSStatusCodesArray();
} else {
if ($message === "") {
$message = "OCS status code is not the expected value " . $statusCodes . " got " . $responseStatusCode;
}
Assert::assertEquals(
$statusCodes,
$responseStatusCode,
$message
);
}
}
/**
* @Then /^the OCS status code should be "([^"]*)" or "([^"]*)"$/
*
* @param string $statusCode1
* @param string $statusCode2
*
* @return void
* @throws Exception
*/
public function theOcsStatusCodeShouldBeOr(string $statusCode1, string $statusCode2): void {
$statusCodes = [$statusCode1,$statusCode1];
$response = $this->featureContext->getResponse();
$responseStatusCode = $this->getOCSResponseStatusCode(
$response
);
Assert::assertContainsEquals(
$responseStatusCode,
$statusCodes,
"OCS status code is not any of the expected values "
. \implode(",", $statusCodes) . " got " . $responseStatusCode
);
$this->featureContext->emptyLastOCSStatusCodesArray();
}
/**
* Check the text in an OCS status message
*
* @Then /^the OCS status message should be "([^"]*)"$/
* @Then /^the OCS status message should be "([^"]*)" in language "([^"]*)"$/
*
* @param string $statusMessage
* @param string|null $language
*
* @return void
*/
public function theOCSStatusMessageShouldBe(string $statusMessage, ?string $language = null): void {
$language = TranslationHelper::getLanguage($language);
$statusMessage = $this->getActualStatusMessage($statusMessage, $language);
Assert::assertEquals(
$statusMessage,
$this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
),
'Unexpected OCS status message :"' . $this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
) . '" in response'
);
}
/**
* Check the text in an OCS status message.
* Use this step form if the expected text contains double quotes,
* single quotes and other content that theOCSStatusMessageShouldBe()
* cannot handle.
*
* After the step, write the expected text in PyString form like:
*
* """
* File "abc.txt" can't be shared due to reason "xyz"
* """
*
* @Then /^the OCS status message should be:$/
*
* @param PyStringNode $statusMessage
*
* @return void
*/
public function theOCSStatusMessageShouldBePyString(
PyStringNode $statusMessage
): void {
Assert::assertEquals(
$statusMessage->getRaw(),
$this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
),
'Unexpected OCS status message: "' . $this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
) . '" in response'
);
}
/**
* Parses the xml answer to get ocs response which doesn't match with
* http one in v1 of the api.
*
* @param ResponseInterface $response
*
* @return string
* @throws Exception
*/
public function getOCSResponseStatusCode(ResponseInterface $response): string {
try {
$jsonResponse = $this->featureContext->getJsonDecodedResponseBodyContent($response);
} catch (JsonException $e) {
$jsonResponse = null;
}
if (\is_object($jsonResponse) && $jsonResponse->ocs->meta->statuscode) {
return (string) $jsonResponse->ocs->meta->statuscode;
}
// go to xml response when json response is null (it means not formatted and get status code)
$responseXmlObject = HttpRequestHelper::getResponseXml($response, __METHOD__);
if (isset($responseXmlObject->meta[0], $responseXmlObject->meta[0]->statuscode)) {
return (string) $responseXmlObject->meta[0]->statuscode;
}
Assert::fail("No OCS status code found in response");
}
/**
* Parses the xml answer to return data items from ocs response
*
* @param ResponseInterface $response
*
* @return SimpleXMLElement
* @throws Exception
*/
public function getOCSResponseData(ResponseInterface $response): SimpleXMLElement {
$responseXmlObject = HttpRequestHelper::getResponseXml($response, __METHOD__);
if (isset($responseXmlObject->data)) {
return $responseXmlObject->data;
}
Assert::fail("No OCS data items found in response");
}
/**
* Parses the xml answer to get ocs response message which doesn't match with
* http one in v1 of the api.
*
* @param ResponseInterface $response
*
* @return string
*/
public function getOCSResponseStatusMessage(ResponseInterface $response): string {
return (string) HttpRequestHelper::getResponseXml($response, __METHOD__)->meta[0]->message;
}
/**
* convert status message in the desired language
*
* @param string $statusMessage
* @param string|null $language
*
* @return string
*/
public function getActualStatusMessage(string $statusMessage, ?string $language = null): string {
if ($language !== null) {
$multiLingualMessage = \json_decode(
\file_get_contents(__DIR__ . "/../fixtures/multiLanguageErrors.json"),
true
);
if (isset($multiLingualMessage[$statusMessage][$language])) {
$statusMessage = $multiLingualMessage[$statusMessage][$language];
}
}
return $statusMessage;
}
/**
* check if the HTTP status code and the OCS status code indicate that the request was successful
* this function is aware of the currently used OCS version
*
* @param string|null $message
* @param ResponseInterface|null $response
*
* @return void
* @throws Exception
*/
public function assertOCSResponseIndicatesSuccess(
?string $message = "",
?ResponseInterface $response = null
): void {
$response = $response ?? $this->featureContext->getResponse();
$this->featureContext->theHTTPStatusCodeShouldBe('200', $message, $response);
if ($this->featureContext->getOcsApiVersion() === 1) {
$this->theOCSStatusCodeShouldBe('100', $message, $response);
} else {
$this->theOCSStatusCodeShouldBe('200', $message, $response);
}
}
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
}
@@ -0,0 +1,218 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2023 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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use TestHelpers\OcConfigHelper;
use TestHelpers\GraphHelper;
use PHPUnit\Framework\Assert;
/**
* steps needed to re-configure КуСфера server
*/
class OcConfigContext implements Context {
private array $enabledPermissionsRoles = [];
/**
* @return array
*/
public function getEnabledPermissionsRoles(): array {
return $this->enabledPermissionsRoles;
}
/**
* @param array $enabledPermissionsRoles
*
* @return void
*/
public function setEnabledPermissionsRoles(array $enabledPermissionsRoles): void {
$this->enabledPermissionsRoles = $enabledPermissionsRoles;
}
/**
* @Given async upload has been enabled with post-processing delayed to :delayTime seconds
*
* @param string $delayTime
*
* @return void
* @throws GuzzleException
*/
public function asyncUploadHasBeenEnabledWithDelayedPostProcessing(string $delayTime): void {
$envs = [
"OC_ASYNC_UPLOADS" => true,
"OC_EVENTS_ENABLE_TLS" => false,
"POSTPROCESSING_DELAY" => $delayTime . "s",
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set async upload with delayed post processing"
);
OcConfigHelper::setPostProcessingDelay($delayTime);
}
/**
* @Given the config :configVariable has been set to :configValue
*
* @param string $configVariable
* @param string $configValue
*
* @return void
* @throws GuzzleException
*/
public function theConfigHasBeenSetTo(string $configVariable, string $configValue): void {
$envs = [
$configVariable => $configValue,
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set config $configVariable=$configValue"
);
if ($configVariable === "POSTPROCESSING_DELAY") {
OcConfigHelper::setPostProcessingDelay($configValue);
}
}
/**
* @Given the administrator has enabled the permissions role :role
*
* @param string $role
*
* @return void
*/
public function theAdministratorHasEnabledTheRole(string $role): void {
$roleId = GraphHelper::getPermissionsRoleIdByName($role);
$defaultRoles = array_values(GraphHelper::DEFAULT_PERMISSIONS_ROLES);
if (!\in_array($role, $defaultRoles)) {
$defaultRoles[] = $roleId;
}
$envs = [
"GRAPH_AVAILABLE_ROLES" => implode(',', $defaultRoles),
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to enable role $role"
);
$this->setEnabledPermissionsRoles($defaultRoles);
}
/**
* @Given the administrator has disabled the permissions role :role
*
* @param string $role
*
* @return void
*/
public function theAdministratorHasDisabledThePermissionsRole(string $role): void {
$roleId = GraphHelper::getPermissionsRoleIdByName($role);
$availableRoles = $this->getEnabledPermissionsRoles();
if ($key = array_search($roleId, $availableRoles)) {
unset($availableRoles[$key]);
}
$envs = [
"GRAPH_AVAILABLE_ROLES" => implode(',', $availableRoles),
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to disable role $role"
);
$this->setEnabledPermissionsRoles($availableRoles);
}
/**
* @Given the config :configVariable has been set to path :path
*
* @param string $configVariable
* @param string $path
*
* @return void
* @throws GuzzleException
*/
public function theConfigHasBeenSetPathTo(string $configVariable, string $path): void {
if (\getenv('TEST_ROOT_PATH')) {
$path = \getenv('TEST_ROOT_PATH') . "/" . $path;
} else {
$path = \realpath(\dirname(__FILE__) . "/../../" . $path);
}
$response = OcConfigHelper::reConfigureOc(
[
$configVariable => $path
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set config $configVariable=$path"
);
}
/**
* @Given the following configs have been set:
*
* @param TableNode $table
*
* @return void
* @throws GuzzleException
*/
public function theConfigHasBeenSetToValue(TableNode $table): void {
$envs = [];
foreach ($table->getHash() as $row) {
$envs[$row['config']] = $row['value'];
if ($row['config'] === "POSTPROCESSING_DELAY") {
OcConfigHelper::setPostProcessingDelay($row['value']);
}
}
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set config"
);
}
/**
* @AfterScenario @env-config
*
* @return void
*/
public function rollbackOc(): void {
OcConfigHelper::setPostProcessingDelay('0');
$response = OcConfigHelper::rollbackOc();
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to rollback КуСфера server. Check if КуСфера is started with ocwrapper."
);
}
}
@@ -0,0 +1,314 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <scharf.vi@gmail.com>
*
* @copyright Copyright (c) 2024, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\OcHelper;
use TestHelpers\OcmHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
/**
* Acceptance test steps related to testing federation share(ocm) features
*/
class OcmContext implements Context {
private FeatureContext $featureContext;
private string $invitationToken;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context from here
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @return string
* @throws Exception
*/
public function getLastFederatedInvitationToken(): string {
if (empty($this->invitationToken)) {
throw new Exception(__METHOD__ . " token not found");
}
return $this->invitationToken;
}
/**
* @param string $user
* @param string $email
* @param string $description
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function createInvitation(string $user, $email = null, $description = null): ResponseInterface {
$response = OcmHelper::createInvitation(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$email,
$description
);
$responseData = \json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (isset($responseData["token"])) {
$this->invitationToken = $responseData["token"];
}
return $response;
}
/**
* @When :user creates the federation share invitation
* @When :user creates the federation share invitation with email :email and description :description
*
* @param string $user
* @param string $email
* @param string $description
*
* @return void
* @throws GuzzleException
*/
public function userCreatesTheFederationShareInvitation(string $user, $email = null, $description = null): void {
$this->featureContext->setResponse($this->createInvitation($user, $email, $description));
}
/**
* @Given :user has created the federation share invitation
* @Given :user has created the federation share invitation with email :email and description :description
*
* @param string $user
* @param string $email
* @param string $description
*
* @return void
* @throws GuzzleException
*/
public function userHasCreatedTheFederationShareInvitation(string $user, $email = null, $description = null): void {
$response = $this->createInvitation($user, $email, $description);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @param string $user
* @param string $token
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function acceptInvitation(string $user, ?string $token = null): ResponseInterface {
$providerDomain = $this->featureContext->getLocalBaseUrlWithoutScheme();
if ($this->featureContext->getCurrentServer() === "LOCAL") {
$providerDomain = $this->featureContext->getRemoteBaseUrlWithoutScheme();
}
return OcmHelper::acceptInvitation(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$token ? $token : $this->getLastFederatedInvitationToken(),
$providerDomain
);
}
/**
* @When :user accepts the last federation share invitation
* @When :user tries to accept the last federation share invitation
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userAcceptsTheLastFederationShareInvitation(string $user): void {
$this->featureContext->setResponse($this->acceptInvitation($user));
}
/**
* @When :user tries to accept the invitation with invalid token
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userTriesToAcceptInvitationWithInvalidToken(string $user): void {
$this->featureContext->setResponse($this->acceptInvitation($user, WebDavHelper::generateUUIDv4()));
}
/**
* @Given :user has accepted invitation
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userHasAcceptedTheLastFederationShareInvitation(string $user): void {
$response = $this->acceptInvitation($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function findAcceptedUsers(string $user): ResponseInterface {
return OcmHelper::findAcceptedUsers(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
}
/**
* @When :user searches for accepted users
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userFindsAcceptedUsers(string $user): void {
$this->featureContext->setResponse($this->findAcceptedUsers($user));
}
/**
*
* @param string $user
* @param string $ocmUserName
*
* @return array
* @throws GuzzleException
*/
public function getAcceptedUserByName(string $user, string $ocmUserName): array {
$users = ($this->featureContext->getJsonDecodedResponse($this->findAcceptedUsers($user)));
foreach ($users as $user) {
if (strpos($user["display_name"], $ocmUserName) !== false) {
return $user;
}
}
throw new \Exception("Could not find user with name '{$ocmUserName}' in the accepted users list.");
}
/**
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function listInvitations(string $user): ResponseInterface {
return OcmHelper::listInvite(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
}
/**
* @When :user lists the created invitations
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userListsCreatedInvitations(string $user): void {
$this->featureContext->setResponse($this->listInvitations($user));
}
/**
* @When the user waits :number seconds for the invitation token to expire
*
* @param int $number
*
* @return void
* @throws GuzzleException
*/
public function theUserWaitsForTokenToExpire(int $number): void {
\sleep($number);
}
/**
* @When user :user deletes federated connection with user :ocmUser using the Graph API
*
* @param string $user
* @param string $ocmUser
*
* @return void
* @throws GuzzleException
*/
public function userDeletesFederatedConnectionWithUserUsingTheGraphApi(string $user, string $ocmUser): void {
$this->featureContext->setResponse($this->deleteConnection($user, $ocmUser));
}
/**
* @When user :user has deleted federated connection with user :ocmUser
*
* @param string $user
* @param string $ocmUser
*
* @return void
* @throws GuzzleException
*/
public function userHasDeletedFederatedConnectionWithUser(string $user, string $ocmUser): void {
$response = $this->deleteConnection($user, $ocmUser);
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"failed while deleting connection with user $ocmUser",
$response
);
}
/**
* @param string $user
* @param string $ocmUser
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function deleteConnection(string $user, string $ocmUser): ResponseInterface {
$ocmUser = $this->getAcceptedUserByName($user, $ocmUser);
return OcmHelper::deleteConnection(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$ocmUser['user_id'],
$ocmUser['idp']
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,281 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2018 Artur Neumann artur@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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\WebDavHelper;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* context containing search related API steps
*/
class SearchContext implements Context {
private FeatureContext $featureContext;
/**
* @param string $user
* @param string $pattern
* @param string|null $limit
* @param string|null $scopeType
* @param string|null $scope
* @param string|null $spaceName
* @param TableNode|null $properties
*
* @return ResponseInterface
* @throws GuzzleException|JsonException
*/
private function searchFiles(
string $user,
string $pattern,
?string $limit = null,
?string $scopeType = null,
?string $scope = null,
?string $spaceName = null,
?TableNode $properties = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$baseUrl = $this->featureContext->getBaseUrl();
$password = $this->featureContext->getPasswordForUser($user);
if (str_contains($pattern, '$')) {
$date = explode("$", $pattern);
switch ($date[1]) {
case "today":
$pattern = $date[0] . date('Y-m-d', strtotime('today'));
break;
case "yesterday":
$pattern = $date[0] . date('Y-m-d', strtotime('yesterday'));
break;
default:
throw new Exception("cannot convert the date");
}
}
$body
= "<?xml version='1.0' encoding='utf-8' ?>\n" .
" <oc:search-files xmlns:a='DAV:' xmlns:oc='http://owncloud.org/ns' >\n" .
" <oc:search>\n";
if ($scope !== null) {
if ($scopeType === "space") {
$spaceId = $this->featureContext->spacesContext->getSpaceIdByName($user, $scope);
$pattern .= " scope:$spaceId";
} else {
$resourceID = $this->featureContext->spacesContext->getResourceId(
$user,
$spaceName ?? "Personal",
$scope
);
$pattern .= " scope:$resourceID";
}
}
$body .= "<oc:pattern>$pattern</oc:pattern>\n";
if ($limit !== null) {
$body .= " <oc:limit>$limit</oc:limit>\n";
}
$body .= " </oc:search>\n";
if ($properties !== null) {
$propertiesRows = $properties->getRows();
$body .= " <a:prop>";
foreach ($propertiesRows as $property) {
$body .= "<$property[0]/>";
}
$body .= " </a:prop>";
}
$body .= " </oc:search-files>";
$davPathVersionToUse = $this->featureContext->getDavPathVersion();
// $davPath will be one of the followings:
// - webdav
// - dav/files
// - dav/spaces
$davPath = WebDavHelper::getDavPath($davPathVersionToUse);
$fullUrl = WebDavHelper::sanitizeUrl("$baseUrl/$davPath");
return HttpRequestHelper::sendRequest(
$fullUrl,
$this->featureContext->getStepLineRef(),
'REPORT',
$user,
$password,
null,
$body
);
}
/**
* @When user :user searches for :pattern using the WebDAV API
* @When user :user searches for :pattern and limits the results to :limit items using the WebDAV API
* @When user :user searches for :pattern using the WebDAV API requesting these properties:
* @When user :user searches for :pattern and limits the results to :limit items using the WebDAV API requesting these properties:
*
* @param string $user
* @param string $pattern
* @param string|null $limit
* @param TableNode|null $properties
*
* @return void
* @throws Exception|GuzzleException
*/
public function userSearchesUsingWebDavAPI(
string $user,
string $pattern,
?string $limit = null,
?TableNode $properties = null
): void {
// NOTE: because indexing of newly uploaded files or directories with КуСфера is decoupled and occurs asynchronously
// short wait is necessary before searching
sleep(10);
$response = $this->searchFiles($user, $pattern, $limit, null, null, null, $properties);
$this->featureContext->setResponse($response);
}
/**
* @Then file/folder :path in the search result of user :user should contain these properties:
*
* @param string $path
* @param string $user
* @param TableNode $properties
*
* @return void
* @throws Exception
*/
public function fileOrFolderInTheSearchResultShouldContainProperties(
string $path,
string $user,
TableNode $properties
): void {
$user = $this->featureContext->getActualUsername($user);
$this->featureContext->verifyTableNodeColumns($properties, ['name', 'value']);
$properties = $properties->getHash();
$fileResult = $this->featureContext->findEntryFromSearchResponse(
$path
);
Assert::assertNotFalse(
$fileResult,
"could not find file/folder '$path'"
);
foreach ($properties as $property) {
$property['value'] = $this->featureContext->substituteInLineCodes(
$property['value'],
$user
);
if (\is_object($fileResult)) {
$fileResultProperty = $fileResult->xpath("d:propstat//" . $property['name']);
} else {
throw new Exception("Expected fileResult to be an object, but found " . \gettype($fileResult));
}
if ($fileResultProperty) {
Assert::assertMatchesRegularExpression(
"/" . $property['value'] . "/",
\trim((string)$fileResultProperty[0])
);
continue;
}
throw new Error("Could not find property '" . $property['name'] . "'");
}
}
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @Then /^the search result should contain these (?:files|entries) with highlight on keyword "([^"]*)"/
*
* @param TableNode $expectedFiles
* @param string $expectedContent
*
* @return void
*
* @throws Exception
*/
public function theSearchResultShouldContainEntriesWithHighlight(
TableNode $expectedFiles,
string $expectedContent
): void {
$this->featureContext->verifyTableNodeColumnsCount($expectedFiles, 1);
$elementRows = $expectedFiles->getRows();
$foundEntries = $this->featureContext->findEntryFromSearchResponse(
null,
true
);
foreach ($elementRows as $expectedFile) {
$filename = $expectedFile[0];
$content = $foundEntries[$filename];
// Extract the content between the <mark> tags
preg_match('/<mark>(.*?)<\/mark>/s', $content, $matches);
$actualContent = $matches[1] ?? '';
// Remove any leading/trailing whitespace for comparison
$actualContent = trim($actualContent);
Assert::assertEquals(
$expectedContent,
$actualContent,
"Expected text highlight to be '$expectedContent' but found '$actualContent'"
);
}
}
/**
* @When /^user "([^"]*)" searches for "([^"]*)" inside (folder|space) "([^"]*)" using the WebDAV API$/
* @When /^user "([^"]*)" searches for "([^"]*)" inside (folder) "([^"]*)" in space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $pattern
* @param string $scopeType
* @param string $scope
* @param string|null $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
public function userSearchesInsideFolderOrSpaceUsingWebDavAPI(
string $user,
string $pattern,
string $scopeType,
string $scope,
?string $spaceName = null,
): void {
// NOTE: since indexing of newly uploaded files or directories with КуСфера is decoupled and occurs asynchronously,
// a short wait is necessary before searching
sleep(5);
$response = $this-> searchFiles($user, $pattern, null, $scopeType, $scope, $spaceName);
$this->featureContext->setResponse($response);
}
}
@@ -0,0 +1,553 @@
<?php
declare(strict_types=1);
/**
* @author Viktor Scharf <v.scharf@owncloud.com>
* @copyright Copyright (c) 2022 Viktor Scharf v.scharf@owncloud.com
*/
use Behat\Behat\Context\Context;
use GuzzleHttp\Exception\GuzzleException;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\SettingsHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Context for the TUS-specific steps using the Graph API
*/
class SettingsContext implements Context {
private FeatureContext $featureContext;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context from here
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @param string $user
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function getRoles(string $user): ResponseInterface {
return SettingsHelper::getRolesList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef()
);
}
/**
* @When /^user "([^"]*)" tries to get all existing roles using the settings API$/
*
* @param string $user
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function getAllExistingRoles(string $user): void {
$response = $this->getRoles($user);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
* @param string $userId
* @param string $roleId
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function assignRoleToUser(string $user, string $userId, string $roleId): ResponseInterface {
return SettingsHelper::assignRoleToUser(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$userId,
$roleId,
$this->featureContext->getStepLineRef(),
);
}
/**
* @param string $user
* @param string $userId
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function getAssignmentsList(string $user, string $userId): ResponseInterface {
return SettingsHelper::getAssignmentsList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$userId,
$this->featureContext->getStepLineRef(),
);
}
/**
* @Given /^the administrator has given "([^"]*)" the role "([^"]*)" using the settings api$/
*
* @param string $user
* @param string $role
*
* @return void
*
* @throws Exception
*/
public function theAdministratorHasGivenUserTheRole(string $user, string $role): void {
$admin = $this->featureContext->getAdminUserName();
$roleId = $this->getRoleIdByRoleName($admin, $role);
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id') ?: $user;
$response = $this->assignRoleToUser($admin, $userId, $roleId);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
}
/**
* @When user :assigner assigns the role :role to user :assignee using the settings API
*
* @param string $assigner
* @param string $role
* @param string $assignee
*
* @return void
*
* @throws Exception
*/
public function userAssignsTheRoleToUserUsingTheSettingsApi(
string $assigner,
string $role,
string $assignee
): void {
$response = $this->assignRoleToUser(
$assigner,
$this->featureContext->getAttributeOfCreatedUser($assignee, 'id'),
$this->getRoleIdByRoleName($assigner, $role)
);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
* @param string $role
*
* @return string
*/
public function getRoleIdByRoleName(string $user, string $role): string {
// Sometimes the response body is not complete and results invalid json.
// So we try again until we get a valid json.
$retried = 0;
do {
$response = $this->getRoles($user);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
$rawBody = $response->getBody()->getContents();
try {
$decodedBody = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
$tryAgain = false;
} catch (Exception $e) {
$tryAgain = $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if (!$tryAgain) {
throw $e;
}
}
if ($tryAgain) {
$retried += 1;
echo "Invalid json body, retrying ($retried)...\n";
// wait 500ms and try again
\usleep(500 * 1000);
}
} while ($tryAgain);
Assert::assertArrayHasKey(
'bundles',
$decodedBody,
__METHOD__ . " could not find bundles in body"
);
$bundles = $decodedBody["bundles"];
$roleToAssign = "";
foreach ($bundles as $value) {
// find the selected role
if ($value["displayName"] === $role) {
$roleToAssign = $value;
break;
}
}
Assert::assertNotEmpty($roleToAssign, "The selected role $role could not be found");
return $roleToAssign["id"];
}
/**
* @When /^user "([^"]*)" changes his own role to "([^"]*)"$/
*
* @param string $user
* @param string $role
*
* @return void
* @throws GuzzleException
*/
public function userChangeOwnRole(string $user, string $role): void {
// we assume that the user knows uuid role.
$roleId = $this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role);
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$response = $this->assignRoleToUser($user, $userId, $roleId);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" changes the role "([^"]*)" for user "([^"]*)"$/
*
* @param string $user
* @param string $role
* @param string $assignedUser
*
* @return void
* @throws GuzzleException
*/
public function userChangeRoleAnotherUser(string $user, string $role, string $assignedUser): void {
// we assume that the user knows uuid role.
$roleId = $this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role);
$userId = $this->featureContext->getAttributeOfCreatedUser($assignedUser, 'id');
$response = $this->assignRoleToUser($user, $userId, $roleId);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" tries to get list of assignment using the settings API$/
*
* @param string $user
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userGetAssignmentsList(string $user): void {
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$this->featureContext->setResponse($this->getAssignmentsList($user, $userId));
}
/**
* @Then /^user "([^"]*)" should have the role "([^"]*)"$/
*
* @param string $user
* @param string $role
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userShouldHaveRole(string $user, string $role): void {
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$response = $this->getAssignmentsList($this->featureContext->getAdminUserName(), $userId);
$assignmentResponse = $this->featureContext->getJsonDecodedResponseBodyContent($response);
if (isset($assignmentResponse->assignments[0]->roleId)) {
$actualRoleId = $assignmentResponse->assignments[0]->roleId;
Assert::assertEquals(
$this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role),
$actualRoleId,
"user $user has no role $role"
);
} else {
Assert::fail("Response should contain user role but not found.\n" . json_encode($assignmentResponse));
}
}
/**
* @Then /^the setting API response should have the role "([^"]*)"$/
*
* @param string $role
*
* @return void
*
* @throws Exception
*/
public function theSettingApiResponseShouldHaveTheRole(string $role): void {
$assignmentRoleId = $this->featureContext->getJsonDecodedResponse(
$this->featureContext->getResponse()
)["assignments"][0]["roleId"];
Assert::assertEquals(
$this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role),
$assignmentRoleId,
"user has no role $role"
);
}
/**
* @param string $user
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestGetBundlesList(string $user): ResponseInterface {
return SettingsHelper::getBundlesList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef(),
);
}
/**
* @param string $user
* @param string $bundleName
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*/
public function getBundleByName(string $user, string $bundleName): array {
return SettingsHelper::getBundleByName(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$bundleName,
$this->featureContext->getStepLineRef()
);
}
/**
* @param string $user
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestGetSettingsValuesList(string $user, array $headers = null): ResponseInterface {
return SettingsHelper::getValuesList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef(),
$headers
);
}
/**
* @When /^user "([^"]*)" lists values-list with headers using the Settings API$/
*
* @param string $user
* @param TableNode $headersTable
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function theUserListsAllValuesListWithHeadersUsingSettingsApi(string $user, TableNode $headersTable): void {
$this->featureContext->verifyTableNodeColumns(
$headersTable,
['header', 'value']
);
$headers = [];
foreach ($headersTable as $row) {
$headers[$row['header']] = $row ['value'];
}
$this->featureContext->setResponse($this->sendRequestGetSettingsValuesList($user, $headers));
}
/**
* @param string $user
* @param string $language
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestToSwitchSystemLanguage(string $user, string $language): ResponseInterface {
$profileBundlesList = $this->getBundleByName($user, "Profile");
Assert::assertNotEmpty($profileBundlesList, "bundles list is empty");
$settingId = '';
foreach ($profileBundlesList["settings"] as $value) {
if ($value["name"] === "language") {
$settingId = $value["id"];
break;
}
}
Assert::assertNotEmpty($settingId, "settingId is empty");
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$body = json_encode(
[
"value" => [
"account_uuid" => "me",
"bundleId" => $profileBundlesList["id"],
"id" => $userId,
"listValue" => [
"values" => [
[
"stringValue" => $language
]
]
],
"resource" => [
"type" => "TYPE_USER"
],
"settingId" => $settingId
]
],
JSON_THROW_ON_ERROR
);
return SettingsHelper::updateSettings(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$body,
$this->featureContext->getStepLineRef()
);
}
/**
* @Given /^user "([^"]*)" has switched the system language to "([^"]*)" using the settings API$/
*
* @param string $user
* @param string $language
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function theUserHasSwitchedSystemLanguage(string $user, string $language): void {
$response = $this->sendRequestToSwitchSystemLanguage($user, $language);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
}
/**
* @When user :user switches the system language to :language using the settings API
*
* @param string $user
* @param string $language
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userSwitchesTheSystemLanguageUsingTheSettingsApi(string $user, string $language): void {
$response = $this->sendRequestToSwitchSystemLanguage($user, $language);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestToDisableAutoAccepting(string $user): ResponseInterface {
$body = json_encode(
[
"value" => [
"account_uuid" => "me",
"bundleId" => "2a506de7-99bd-4f0d-994e-c38e72c28fd9",
"settingId" => "ec3ed4a3-3946-4efc-8f9f-76d38b12d3a9",
"resource" => [
"type" => "TYPE_USER"
],
"boolValue" => false
]
],
JSON_THROW_ON_ERROR
);
return SettingsHelper::updateSettings(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$body,
$this->featureContext->getStepLineRef()
);
}
/**
* @Given user :user has disabled auto-accepting
* @Given user :user has disabled the auto-sync share
*
* @param string $user
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function theUserHasDisabledAutoAccepting(string $user): void {
$response = $this->sendRequestToDisableAutoAccepting($user);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
$this->featureContext->rememberUserAutoSyncSetting($user, false);
}
/**
* @When user :user disables the auto-sync share using the settings API
*
* @param string $user
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userDisablesAutoAcceptingUsingSettingsApi(string $user): void {
$response = $this->sendRequestToDisableAutoAccepting($user);
$this->featureContext->setResponse($response);
$this->featureContext->rememberUserAutoSyncSetting($user, false);
}
}
@@ -0,0 +1,221 @@
<?php declare(strict_types=1);
/**
* @author Joas Schilling <coding@schilljs.com>
* @author Sergio Bertolin <sbertolin@owncloud.com>
* @author Phillip Davis <phil@jankaritech.com>
* @copyright Copyright (c) 2018, ownCloud GmbH
*
* 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/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
use TestHelpers\BehatHelper;
use TestHelpers\HttpRequestHelper;
require_once 'bootstrap.php';
/**
* Sharees context.
*/
class ShareesContext implements Context {
private FeatureContext $featureContext;
private OCSContext $ocsContext;
/**
* @When /^user "([^"]*)" gets the sharees using the sharing API with parameters$/
*
* @param string $user
* @param TableNode $body
*
* @return void
*/
public function userGetsTheShareesWithParameters(string $user, TableNode $body): void {
$this->featureContext->setResponse(
$this->getShareesWithParameters(
$user,
$body
)
);
}
/**
* @Then /^the "([^"]*)" sharees returned should be$/
*
* @param string $shareeType
* @param TableNode $shareesList
*
* @return void
* @throws Exception
*/
public function theShareesReturnedShouldBe(string $shareeType, TableNode $shareesList): void {
$this->featureContext->verifyTableNodeColumnsCount($shareesList, 4);
$sharees = $shareesList->getRows();
$respondedArray = $this->getArrayOfShareesResponded(
$this->featureContext->getResponse(),
$shareeType
);
Assert::assertEquals(
$sharees,
$respondedArray,
"Returned sharees do not match the expected ones. See the differences below."
);
}
/**
* @Then /^the "([^"]*)" sharees returned should include$/
*
* @param string $shareeType
* @param TableNode $shareesList
*
* @return void
* @throws Exception
*/
public function theShareesReturnedShouldInclude(string $shareeType, TableNode $shareesList): void {
$this->featureContext->verifyTableNodeColumnsCount($shareesList, 3);
$sharees = $shareesList->getRows();
$respondedArray = $this->getArrayOfShareesResponded(
$this->featureContext->getResponse(),
$shareeType
);
foreach ($sharees as $sharee) {
Assert::assertContains(
$sharee,
$respondedArray,
"Returned sharees do not match the expected ones. See the differences below."
);
}
}
/**
* @Then /^the "([^"]*)" sharees returned should be empty$/
*
* @param string $shareeType
*
* @return void
*/
public function theShareesReturnedShouldBeEmpty(string $shareeType): void {
$respondedArray = $this->getArrayOfShareesResponded(
$this->featureContext->getResponse(),
$shareeType
);
if (isset($respondedArray[0])) {
// [0] is display name and [2] is user or group id
$firstEntry = $respondedArray[0][0] . " (" . $respondedArray[0][2] . ")";
} else {
$firstEntry = "";
}
Assert::assertEmpty(
$respondedArray,
"'$shareeType' array should be empty, but it starts with $firstEntry"
);
}
/**
* @param ResponseInterface $response
* @param string $shareeType
*
* @return array
* @throws Exception
*/
public function getArrayOfShareesResponded(
ResponseInterface $response,
string $shareeType
): array {
$elements = HttpRequestHelper::getResponseXml($response, __METHOD__)->data;
$elements = \json_decode(\json_encode($elements), true);
if (\strpos($shareeType, 'exact ') === 0) {
$elements = $elements['exact'];
$shareeType = \substr($shareeType, 6);
}
Assert::assertArrayHasKey(
$shareeType,
$elements,
__METHOD__ . " The sharees response does not have key '$shareeType'"
);
$sharees = [];
foreach ($elements[$shareeType] as $element) {
if (\is_int(\key($element))) {
// this is a list of elements instead of just one item,
// so return the list
foreach ($element as $innerItem) {
$sharees[] = [
$innerItem['label'],
$innerItem['value']['shareType'],
$innerItem['value']['shareWith'],
$innerItem['value']['shareWithAdditionalInfo']
];
}
} else {
$sharees[] = [
$element['label'],
$element['value']['shareType'],
$element['value']['shareWith'],
$element['value']['shareWithAdditionalInfo']
];
}
}
return $sharees;
}
/**
* @param string $user
* @param TableNode $body
*
* @return ResponseInterface
*/
public function getShareesWithParameters(string $user, TableNode $body): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$url = '/apps/files_sharing/api/v1/sharees';
$this->featureContext->verifyTableNodeColumnsCount($body, 2);
$parameters = [];
foreach ($body->getRowsHash() as $key => $value) {
$parameters[] = "$key=$value";
}
if (!empty($parameters)) {
$url .= '?' . \implode('&', $parameters);
}
return $this->ocsContext->sendRequestToOcsEndpoint(
$user,
'GET',
$url
);
}
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->ocsContext = BehatHelper::getContext($scope, $environment, 'OCSContext');
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,460 @@
<?php
declare(strict_types=1);
/**
* @author Viktor Scharf <v.scharf@owncloud.com>
* @copyright Copyright (c) 2022 Viktor Scharf v.scharf@owncloud.com
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Context for the TUS-specific steps using the Graph API
*/
class SpacesTUSContext implements Context {
private FeatureContext $featureContext;
private TUSContext $tusContext;
private SpacesContext $spacesContext;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context from here
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
$this->tusContext = BehatHelper::getContext($scope, $environment, 'TUSContext');
}
/**
* @Given /^user "([^"]*)" has uploaded a file from "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $spaceName
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userHasUploadedFileViaTusInSpace(
string $user,
string $source,
string $destination,
string $spaceName
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$this->tusContext->uploadFileUsingTus($user, $source, $destination, $spaceId);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @When /^user "([^"]*)" uploads a file from "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $spaceName
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsAFileViaTusInsideOfTheSpaceUsingTheWebdavApi(
string $user,
string $source,
string $destination,
string $spaceName
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$this->tusContext->uploadFileUsingTus($user, $source, $destination, $spaceId);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @Given user :user has created a new TUS resource in the space :spaceName with the following headers:
*
* @param string $user
* @param string $spaceName
* @param TableNode $headers
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userHasCreatedANewTusResourceForTheSpaceUsingTheWebdavApiWithTheseHeaders(
string $user,
string $spaceName,
TableNode $headers
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$response = $this->tusContext->createNewTUSResourceWithHeaders($user, $headers, '', $spaceId);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "Expected response status code should be 201", $response);
}
/**
* @When user :user creates a new TUS resource for the space :spaceName with content :content using the WebDAV API with these headers:
*
* @param string $user
* @param string $spaceName
* @param string $content
* @param TableNode $headers
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userCreatesANewTusResourceForTheSpaceUsingTheWebdavApiWithTheseHeaders(
string $user,
string $spaceName,
string $content,
TableNode $headers
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$response = $this->tusContext->createNewTUSResourceWithHeaders($user, $headers, $content, $spaceId);
$this->featureContext->setResponse($response);
}
/**
* Uploads a file with content to the specified space using the TUS protocol via the WebDAV API.
*
* @param string $user
* @param string $content
* @param string $resource
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
private function uploadFileViaTus(string $user, string $content, string $resource, string $spaceName): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$tmpFile = $this->tusContext->writeDataToTempFile($content);
try {
$this->tusContext->uploadFileUsingTus(
$user,
\basename($tmpFile),
$resource,
$spaceId
);
$this->featureContext->setLastUploadDeleteTime(\time());
} catch (Exception $e) {
Assert::assertStringContainsString('Unable to create resource', (string)$e);
}
\unlink($tmpFile);
}
/**
* @When /^user "([^"]*)" uploads a file with content "([^"]*)" to "([^"]*)" inside federated share "([^"]*)" via TUS using the WebDAV API$/
*
* @param string $user
* @param string $content
* @param string $file
* @param string $destination
*
* @return void
* @throws Exception|GuzzleException
*/
public function userUploadsAFileWithContentToInsideFederatedShareViaTusUsingTheWebdavApi(
string $user,
string $content,
string $file,
string $destination
): void {
$remoteItemId = $this->spacesContext->getSharesRemoteItemId($user, $destination);
$remoteItemId = \rawurlencode($remoteItemId);
$tmpFile = $this->tusContext->writeDataToTempFile($content);
$this->tusContext->uploadFileUsingTus(
$user,
\basename($tmpFile),
$file,
$remoteItemId
);
$this->featureContext->setLastUploadDeleteTime(\time());
\unlink($tmpFile);
}
/**
* @When /^user "([^"]*)" uploads a file with content "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $content
* @param string $resource
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
public function userUploadsAFileWithContentToViaTusInsideOfTheSpaceUsingTheWebdavApi(
string $user,
string $content,
string $resource,
string $spaceName
): void {
$this->uploadFileViaTus($user, $content, $resource, $spaceName);
}
/**
* @Given /^user "([^"]*)" has uploaded a file with content "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)"$/
*
* @param string $user
* @param string $content
* @param string $resource
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
public function userHasUploadedAFileWithContentToViaTusInsideOfTheSpace(
string $user,
string $content,
string $resource,
string $spaceName
): void {
$this->uploadFileViaTus($user, $content, $resource, $spaceName);
}
/**
* @When /^user "([^"]*)" uploads a file "([^"]*)" to "([^"]*)" with mtime "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API
* @param string $spaceName
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsAFileToWithMtimeViaTusInsideOfTheSpaceUsingTheWebdavApi(
string $user,
string $source,
string $destination,
string $mtime,
string $spaceName
): void {
switch ($mtime) {
case "today":
$mtime = date('Y-m-d', strtotime('today'));
break;
case "yesterday":
$mtime = date('Y-m-d', strtotime('yesterday'));
break;
case "lastWeek":
$mtime = date('Y-m-d', strtotime('-7 days'));
break;
case "lastMonth":
$mtime = date('Y-m-d', strtotime('first day of previous month'));
break;
case "lastYear":
$mtime = date('Y-m' . '-01', strtotime('-1 year'));
break;
default:
}
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$mtime = new DateTime($mtime);
$mtime = $mtime->format('U');
$user = $this->featureContext->getActualUsername($user);
$this->tusContext->uploadFileUsingTus(
$user,
$source,
$destination,
$spaceId,
['mtime' => $mtime]
);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @Given /^user "([^"]*)" has uploaded file with checksum "([^"]*)" to the last created TUS Location with offset "([^"]*)" and content "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
* @codingStandardsIgnoreStart
*/
public function userHasUploadedFileWithChecksumToTheLastCreatedTusLocationWithOffsetAndContentViaTusInsideOfTheSpaceUsingTheWebdavApi(
// @codingStandardsIgnoreEnd
string $user,
string $checksum,
string $offset,
string $content,
string $spaceName
): void {
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When /^user "([^"]*)" uploads file with checksum "([^"]*)" to the last created TUS Location with offset "([^"]*)" and content "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
* @codingStandardsIgnoreStart
*/
public function userUploadsFileWithChecksumToTheLastCreatedTusLocationWithOffsetAndContentViaTusInsideOfTheSpaceUsingTheWebdavApi(
// @codingStandardsIgnoreEnd
string $user,
string $checksum,
string $offset,
string $content,
string $spaceName
): void {
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sends a chunk to the last created TUS Location with offset "([^"]*)" and data "([^"]*)" with checksum "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
* @codingStandardsIgnoreStart
*/
public function userSendsAChunkToTheLastCreatedTusLocationWithOffsetAndDataWithChecksumViaTusInsideOfTheSpaceUsingTheWebdavApi(
// @codingStandardsIgnoreEnd
string $user,
string $offset,
string $data,
string $checksum,
string $spaceName
): void {
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sends a chunk to the last created TUS Location with data "([^"]*)" with the following headers:$/
*
* @param string $user
* @param string $data
* @param TableNode $headers
*
* @return void
* @throws Exception|GuzzleException
*/
public function userSendsAChunkToTheLastCreatedTusLocationWithDataInsideOfTheSpaceWithHeaders(
string $user,
string $data,
TableNode $headers
): void {
$rows = $headers->getRowsHash();
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation(
$user,
$resourceLocation,
$rows['Upload-Offset'],
$data,
$rows['Upload-Checksum'],
['Origin' => $rows['Origin']]
);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" overwrites recently shared file with offset "([^"]*)" and data "([^"]*)" with checksum "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API with these headers:$/
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
* @param string $spaceName
* @param TableNode $headers
*
* @return void
* @throws GuzzleException
* @codingStandardsIgnoreStart
*/
public function userOverwritesRecentlySharedFileWithOffsetAndDataWithChecksumViaTusInsideOfTheSpaceUsingTheWebdavApiWithTheseHeaders(
// @codingStandardsIgnoreEnd
string $user,
string $offset,
string $data,
string $checksum,
string $spaceName,
TableNode $headers
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$createResponse = $this->tusContext->createNewTUSResource($user, $headers, $spaceId);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $createResponse);
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @Then /^as "([^"]*)" the mtime of the file "([^"]*)" in space "([^"]*)" should be "([^"]*)"$/
*
* @param string $user
* @param string $resource
* @param string $spaceName
* @param string $mtime
*
* @return void
* @throws Exception|GuzzleException
*/
public function theMtimeOfTheFileInSpaceShouldBe(
string $user,
string $resource,
string $spaceName,
string $mtime
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$mtime = new DateTime($mtime);
Assert::assertEquals(
$mtime->format('U'),
WebDavHelper::getMtimeOfResource(
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$resource,
$this->featureContext->getStepLineRef(),
$this->featureContext->getDavPathVersion(),
$spaceId,
)
);
}
}
@@ -0,0 +1,746 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
*
* @copyright Copyright (c) 2020, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
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;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
/**
* TUS related test steps
*/
class TUSContext implements Context {
private FeatureContext $featureContext;
private array $tusResourceLocations = [];
/**
* @param string $filenameHash
* @param string $location
*
* @return void
*/
public function saveTusResourceLocation(string $filenameHash, string $location): void {
$this->tusResourceLocations[$filenameHash][] = $location;
}
/**
* @param string $filenameHash
* @param int|null $index
*
* @return string
*/
public function getTusResourceLocation(string $filenameHash, ?int $index = null): string {
if ($index === null) {
// get the last one
$index = \count($this->tusResourceLocations[$filenameHash]) - 1;
}
return $this->tusResourceLocations[$filenameHash][$index];
}
/**
* @return string
*/
public function getLastTusResourceLocation(): string {
$lastKey = \array_key_last($this->tusResourceLocations);
$index = \count($this->tusResourceLocations[$lastKey]) - 1;
return $this->tusResourceLocations[$lastKey][$index];
}
/**
* @param string $uploadMetadata
*
* @return string
*/
public function parseFilenameHash(string $uploadMetadata): string {
$filenameHash = \explode("filename ", $uploadMetadata)[1] ?? '';
return \explode(" ", $filenameHash, 2)[0];
}
/**
* @param string $user
* @param TableNode $headersTable
* @param string $content
* @param string|null $spaceId
*
* @return ResponseInterface
*
* @throws Exception
* @throws GuzzleException
*/
public function createNewTUSResourceWithHeaders(
string $user,
TableNode $headersTable,
string $content = '',
?string $spaceId = null
): ResponseInterface {
$this->featureContext->verifyTableNodeColumnsCount($headersTable, 2);
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getUserPassword($user);
$headers = $headersTable->getRowsHash();
$response = $this->featureContext->makeDavRequest(
$user,
"POST",
null,
$headers,
$content,
$spaceId,
"files",
null,
false,
$password
);
$locationHeader = $response->getHeader('Location');
if (\sizeof($locationHeader) > 0) {
$filenameHash = $this->parseFilenameHash($headers['Upload-Metadata']);
$this->saveTusResourceLocation($filenameHash, $locationHeader[0]);
}
return $response;
}
/**
* @When user :user creates a new TUS resource on the WebDAV API with these headers:
*
* @param string $user
* @param TableNode $headers
* @param string $content
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userCreateNewTUSResourceWithHeaders(string $user, TableNode $headers, string $content = ''): void {
$response = $this->createNewTUSResourceWithHeaders($user, $headers, $content);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has created a new TUS resource on the WebDAV API with these headers:
*
* @param string $user
* @param TableNode $headers Tus-Resumable: 1.0.0 header is added automatically
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userHasCreatedNewTUSResourceWithHeaders(string $user, TableNode $headers): void {
$response = $this->createNewTUSResource($user, $headers);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $response);
}
/**
* @param string $user
* @param TableNode $headers
* @param string|null $spaceId
*
* @return ResponseInterface
*/
public function createNewTUSResource(string $user, TableNode $headers, ?string $spaceId = null): ResponseInterface {
$rows = $headers->getRows();
$rows[] = ['Tus-Resumable', '1.0.0'];
return $this->createNewTUSResourceWithHeaders($user, new TableNode($rows), '', $spaceId);
}
/**
* @param string $user
* @param string $resourceLocation
* @param string $offset
* @param string $data
* @param string $checksum
* @param array|null $extraHeaders
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws JsonException
*/
public function uploadChunkToTUSLocation(
string $user,
string $resourceLocation,
string $offset,
string $data,
string $checksum = '',
?array $extraHeaders = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getUserPassword($user);
$headers = [
'Content-Type' => 'application/offset+octet-stream',
'Tus-Resumable' => '1.0.0',
'Upload-Checksum' => $checksum,
'Upload-Offset' => $offset
];
$headers = empty($extraHeaders) ? $headers : array_merge($headers, $extraHeaders);
return HttpRequestHelper::sendRequest(
$resourceLocation,
$this->featureContext->getStepLineRef(),
'PATCH',
$user,
$password,
$headers,
$data
);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with retry on offset mismatch using the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userSendsAChunkToTUSLocationWithOffsetAndDataWithRetryOnOffsetMismatch(
string $user,
string $offset,
string $data,
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$retried = 0;
do {
$tryAgain = false;
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
// retry on 409 Conflict (Offset mismatch during TUS upload)
if ($response->getStatusCode() === 409) {
$tryAgain = true;
}
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "Offset mismatch during TUS upload, retrying ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
$this->featureContext->setResponse($response);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data using the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userSendsAChunkToTUSLocationWithOffsetAndData(string $user, string $offset, string $data): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
$this->featureContext->setResponse($response);
}
/**
* @When user :user uploads file :source to :destination using the TUS protocol on the WebDAV API
*
* @param string|null $user
* @param string $source
* @param string $destination
* @param array $uploadMetadata array of metadata to be placed in the
* `Upload-Metadata` header.
* see https://tus.io/protocols/resumable-upload.html#upload-metadata
* Don't Base64 encode the value.
* @param int $noOfChunks
* @param int|null $bytes
* @param string $checksum
*
* @return void
* @throws ConnectionException
* @throws GuzzleException
* @throws JsonException
* @throws ReflectionException
* @throws TusException
*/
public function userUploadsUsingTusAFileTo(
?string $user,
string $source,
string $destination,
array $uploadMetadata = [],
int $noOfChunks = 1,
?int $bytes = null,
string $checksum = ''
): void {
$this->uploadFileUsingTus($user, $source, $destination, null, $uploadMetadata, $noOfChunks, $bytes, $checksum);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @param string $user
* @param string $source
* @param string $destination
* @param string|null $spaceId
* @param array $uploadMetadata
* @param integer $noOfChunks
* @param integer $bytes
* @param string $checksum
*
* @return void
*/
public function uploadFileUsingTus(
?string $user,
string $source,
string $destination,
?string $spaceId = null,
array $uploadMetadata = [],
int $noOfChunks = 1,
?int $bytes = null,
string $checksum = ''
) {
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getUserPassword($user);
$headers = [
'Authorization' => 'Basic ' . \base64_encode($user . ':' . $password)
];
if ($bytes !== null) {
$creationWithUploadHeader = [
'Content-Type' => 'application/offset+octet-stream',
'Tus-Resumable' => '1.0.0'
];
$headers = \array_merge($headers, $creationWithUploadHeader);
}
if ($checksum != '') {
$checksumHeader = [
'Upload-Checksum' => $checksum
];
$headers = \array_merge($headers, $checksumHeader);
}
$client = new Client(
$this->featureContext->getBaseUrl(),
[
'verify' => false,
'headers' => $headers
]
);
$davPathVersion = $this->featureContext->getDavPathVersion();
$suffixPath = $user;
if ($davPathVersion === WebDavHelper::DAV_VERSION_SPACES) {
$suffixPath = $spaceId ?: $this->featureContext->getPersonalSpaceIdForUser($user);
}
$client->setChecksumAlgorithm('sha1');
$client->setApiPath(WebDavHelper::getDavPath($davPathVersion, $suffixPath));
$client->setMetadata($uploadMetadata);
$sourceFile = UploadHelper::getAcceptanceTestsDir() . $source;
$client->setKey((string)rand())->file($sourceFile, $destination);
$this->featureContext->pauseUploadDelete();
if ($bytes !== null) {
$client->file($sourceFile, $destination)->createWithUpload($client->getKey(), $bytes);
} elseif (\filesize($sourceFile) === 0) {
$client->file($sourceFile, $destination)->createWithUpload($client->getKey(), 0);
} elseif ($noOfChunks === 1) {
$client->file($sourceFile, $destination)->upload();
} else {
$bytesPerChunk = (int)\ceil(\filesize($sourceFile) / $noOfChunks);
for ($i = 0; $i < $noOfChunks; $i++) {
$client->upload($bytesPerChunk);
}
}
}
/**
* @When user :user uploads file with content :content to :destination using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $content
* @param string $destination
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public function userUploadsAFileWithContentToUsingTus(
string $user,
string $content,
string $destination
): void {
$temporaryFileName = $this->writeDataToTempFile($content);
try {
$this->uploadFileUsingTus(
$user,
\basename($temporaryFileName),
$destination
);
$this->featureContext->setLastUploadDeleteTime(\time());
} catch (Exception $e) {
Assert::assertStringContainsString('TusPhp\Exception\FileException: Unable to create resource', (string)$e);
}
\unlink($temporaryFileName);
}
/**
* @When user :user uploads file with content :content in :noOfChunks chunks to :destination using the TUS protocol on the WebDAV API
*
* @param string|null $user
* @param string $content
* @param int|null $noOfChunks
* @param string $destination
*
* @return void
* @throws ConnectionException
* @throws GuzzleException
* @throws JsonException
* @throws ReflectionException
* @throws TusException
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsAFileWithContentInChunksUsingTus(
?string $user,
string $content,
?int $noOfChunks,
string $destination
): void {
$temporaryFileName = $this->writeDataToTempFile($content);
$this->uploadFileUsingTus(
$user,
\basename($temporaryFileName),
$destination,
null,
[],
$noOfChunks
);
$this->featureContext->setLastUploadDeleteTime(\time());
\unlink($temporaryFileName);
}
/**
* @Given user :user has uploaded file :source to :destination with mtime :mtime using the TUS protocol
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userHasUploadedFileWithMtimeUsingTUS(
string $user,
string $source,
string $destination,
string $mtime
): void {
$mtime = new DateTime($mtime);
$mtime = $mtime->format('U');
$user = $this->featureContext->getActualUsername($user);
$this->uploadFileUsingTus(
$user,
$source,
$destination,
null,
['mtime' => $mtime]
);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @When user :user uploads file :source to :destination with mtime :mtime using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsFileWithContentToWithMtimeUsingTUS(
string $user,
string $source,
string $destination,
string $mtime
): void {
$mtime = new DateTime($mtime);
$mtime = $mtime->format('U');
$user = $this->featureContext->getActualUsername($user);
$this->uploadFileUsingTus(
$user,
$source,
$destination,
null,
['mtime' => $mtime]
);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @param string $content
*
* @return string the file name
* @throws Exception
*/
public function writeDataToTempFile(string $content): string {
$temporaryFileName = \tempnam(
UploadHelper::getAcceptanceTestsDir(),
"tus-upload-test-"
);
if ($temporaryFileName === false) {
throw new \Exception("could not create a temporary filename");
}
$temporaryFile = \fopen($temporaryFileName, "w");
if ($temporaryFile === false) {
throw new \Exception("could not open " . $temporaryFileName . " for write");
}
\fwrite($temporaryFile, $content);
\fclose($temporaryFile);
return $temporaryFileName;
}
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
// clear TUS locations cache
$this->tusResourceLocations = [];
}
/**
* @When user :user creates a new TUS resource with content :content on the WebDAV API with these headers:
*
* @param string $user
* @param string $content
* @param TableNode $headers
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userCreatesWithUpload(
string $user,
string $content,
TableNode $headers
): void {
$response = $this->createNewTUSResourceWithHeaders($user, $headers, $content);
$this->featureContext->setResponse($response);
}
/**
* @When user :user creates file :source and uploads content :content in the same request using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $source
* @param string $content
*
* @return void
* @throws Exception
*/
public function userUploadsWithCreatesWithUpload(
string $user,
string $source,
string $content
): void {
$temporaryFileName = $this->writeDataToTempFile($content);
$this->uploadFileUsingTus(
$user,
\basename($temporaryFileName),
$source,
null,
[],
1,
-1
);
$this->featureContext->setLastUploadDeleteTime(\time());
\unlink($temporaryFileName);
}
/**
* @When user :user uploads file with checksum :checksum to the last created TUS Location with offset :offset and content :content using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
*
* @return void
* @throws Exception
*/
public function userUploadsFileWithChecksum(
string $user,
string $checksum,
string $offset,
string $content
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @When user :user uploads content :content with checksum :checksum and offset :offset to the index :locationIndex location of file :filename using the TUS protocol
* @When user :user tries to upload content :content with checksum :checksum and offset :offset to the index :locationIndex location of file :filename using the TUS protocol
*
* @param string $user
* @param string $content
* @param string $checksum
* @param string $offset
* @param string $locationIndex
* @param string $filename
*
* @return void
* @throws Exception
*/
public function userUploadsContentWithChecksumAndOffsetToIndexLocationUsingTUSProtocol(
string $user,
string $content,
string $checksum,
string $offset,
string $locationIndex,
string $filename
): void {
$filenameHash = \base64_encode($filename);
$resourceLocation = $this->getTusResourceLocation($filenameHash, (int)$locationIndex);
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has uploaded file with checksum :checksum to the last created TUS Location with offset :offset and content :content using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
*
* @return void
* @throws Exception
*/
public function userHasUploadedFileWithChecksum(
string $user,
string $checksum,
string $offset,
string $content
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
*
* @return void
* @throws Exception
*/
public function userUploadsChunkFileWithChecksum(
string $user,
string $offset,
string $data,
string $checksum
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has uploaded a chunk to the last created TUS Location with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
*
* @return void
* @throws Exception
*/
public function userHasUploadedChunkFileWithChecksum(
string $user,
string $offset,
string $data,
string $checksum
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When user :user overwrites recently shared file with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API with these headers:
* @When user :user overwrites existing file with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API with these headers:
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
* @param TableNode $headers Tus-Resumable: 1.0.0 header is added automatically
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userOverwritesFileWithChecksum(
string $user,
string $offset,
string $data,
string $checksum,
TableNode $headers
): void {
$createResponse = $this->createNewTUSResource($user, $headers);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $createResponse);
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
}
@@ -0,0 +1,308 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <scharf.vi@gmail.com>
*
* @copyright Copyright (c) 2022, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\GraphHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Acceptance test steps related to testing tags features
*/
class TagContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* This will run before EVERY scenario.
* It will set the properties for this object.
*
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*/
public function before(BeforeScenarioScope $scope): void {
// Get the environment
$environment = $scope->getEnvironment();
// Get all the contexts you need in this context from here
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return ResponseInterface
* @throws Exception
*/
public function createTags(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): ResponseInterface {
$tagNameArray = [];
foreach ($table->getRows() as $value) {
$tagNameArray[] = $value[0];
}
if ($fileOrFolder === 'folder' || $fileOrFolder === 'folders') {
$resourceId = $this->spacesContext->getResourceId($user, $space, $resource);
} else {
$resourceId = $this->spacesContext->getFileId($user, $space, $resource);
}
return GraphHelper::createTags(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$resourceId,
$tagNameArray
);
}
/**
* @When /^user "([^"]*)" creates the following tags for (folder|file) "([^"]*)" of space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theUserCreatesFollowingTags(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->createTags($user, $fileOrFolder, $resource, $space, $table);
$this->featureContext->setResponse($response);
}
/**
* @Given /^user "([^"]*)" has created the following tags for (folder|file)\s?"([^"]*)" of the space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theUserHasCreatedFollowingTags(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->createTags($user, $fileOrFolder, $resource, $space, $table);
$this->featureContext->theHttpStatusCodeShouldBe(200, "", $response);
}
/**
* @Given /^user "([^"]*)" has tagged the following (folders|files) of the space "([^"]*)":$/
*
* @param string $user
* @param string $filesOrFolders (files|folders)
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userHasCreatedTheFollowingTagsForFilesOfTheSpace(
string $user,
string $filesOrFolders,
string $space,
TableNode $table
): void {
$this->featureContext->verifyTableNodeColumns($table, ["path", "tagName"]);
$rows = $table->getHash();
foreach ($rows as $row) {
$tags = explode(',', $row['tagName']);
$response = $this->createTags($user, $filesOrFolders, $row['path'], $space, new TableNode([$tags]));
$this->featureContext->theHttpStatusCodeShouldBe(200, "", $response);
}
}
/**
* @When user :user lists all available tag(s) via the Graph API
*
* @param string $user
*
* @return void
* @throws Exception
*/
public function theUserGetsAllAvailableTags(string $user): void {
// Note: after creating or deleting tags, in some cases tags do not appear or disappear immediately,
// So wait is necessary before listing tags
sleep(5);
$this->featureContext->setResponse(
GraphHelper::getTags(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef()
)
);
}
/**
* @Then /^the response should (not|)\s?contain following tag(s):$/
*
* @param string $shouldOrNot (not|)
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theFollowingTagsShouldExistForUser(string $shouldOrNot, TableNode $table): void {
$rows = $table->getRows();
foreach ($rows as $row) {
$responseArray = $this->featureContext->getJsonDecodedResponse(
$this->featureContext->getResponse()
)['value'];
if ($shouldOrNot === "not") {
Assert::assertFalse(
\in_array($row[0], $responseArray),
"the response should not contain the tag $row[0].\nResponse\n"
. print_r($responseArray, true)
);
} else {
Assert::assertTrue(
\in_array($row[0], $responseArray),
"the response does not contain the tag $row[0].\nResponse\n"
. print_r($responseArray, true)
);
}
}
}
/**
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return ResponseInterface
* @throws Exception
*/
public function removeTagsFromResourceOfTheSpace(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): ResponseInterface {
$tagNameArray = [];
foreach ($table->getRows() as $value) {
$tagNameArray[] = $value[0];
}
if ($fileOrFolder === 'folder') {
$resourceId = $this->spacesContext->getResourceId($user, $space, $resource);
} else {
$resourceId = $this->spacesContext->getFileId($user, $space, $resource);
}
return GraphHelper::deleteTags(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$resourceId,
$tagNameArray
);
}
/**
* @When /^user "([^"]*)" removes the following tags for (folder|file) "([^"]*)" of space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRemovesTagsFromResourceOfTheSpace(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->removeTagsFromResourceOfTheSpace(
$user,
$fileOrFolder,
$resource,
$space,
$table
);
$this->featureContext->setResponse($response);
}
/**
* @Given /^user "([^"]*)" has removed the following tags for (folder|file) "([^"]*)" of space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userHAsRemovedTheFollowingTagsForFileOfSpace(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->removeTagsFromResourceOfTheSpace($user, $fileOrFolder, $resource, $space, $table);
$this->featureContext->theHttpStatusCodeShouldBe(200, "", $response);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* @author Phil Davis <phil@jankaritech.com>
* @copyright Copyright (c) 2020 Phil Davis phil@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/>
*
*/
use Composer\Autoload\ClassLoader;
$classLoader = new ClassLoader();
$classLoader->addPsr4("TestHelpers\\", __DIR__ . "/../TestHelpers", true);
$classLoader->register();
// Default number of times to retry where retries are useful
if (!\defined('STANDARD_RETRY_COUNT')) {
\define('STANDARD_RETRY_COUNT', 10);
}
// Minimum number of times to retry where retries are useful
if (!\defined('MINIMUM_RETRY_COUNT')) {
\define('MINIMUM_RETRY_COUNT', 2);
}
// Minimum number of times to retry where retries are useful
if (!\defined('HTTP_REQUEST_TIMEOUT')) {
\define('HTTP_REQUEST_TIMEOUT', 60);
}
// The remote server-under-test might or might not happen to have this directory.
// If it does not exist, then the tests may end up creating it.
if (!\defined('ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER')) {
\define('ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER', 'tests/acceptance');
}
// The following directory should NOT already exist on the remote server-under-test.
// Acceptance tests are free to do anything needed in this directory, and to
// delete it during or at the end of testing.
if (!\defined('TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER')) {
\define('TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER', ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER . '/server_tmp');
}
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# when deleting the tests suites from /features there might be the tests scenarios that might be in the expected to failure file
# this script checks if there are such scenarios in the expected to failure file which needs to be deleted
# helper functions
log_error() {
echo -e "\e[31m$1\e[0m"
}
log_info() {
echo -e "\e[37m$1\e[0m"
}
log_success() {
echo -e "\e[32m$1\e[0m"
}
SCRIPT_PATH=$(dirname "$0")
PATH_TO_SUITES="${SCRIPT_PATH}/features"
EXPECTED_FAILURE_FILES=("expected-failures-decomposed-storage.md" "expected-failures-posix-storage.md" "expected-failures-without-remotephp.md")
# contains all the suites names inside tests/acceptance/features
AVAILABLE_SUITES=($(ls -l "$PATH_TO_SUITES" | grep '^d' | awk '{print $NF}'))
# regex to match [someSuites/someFeatureFile.feature:lineNumber]
SCENARIO_REGEX="\[([a-zA-Z0-9]+/[a-zA-Z0-9]+\.feature:[0-9]+)]"
# contains all those suites available in the expected to failure files in pattern [someSuites/someFeatureFile.feature:lineNumber]
EXPECTED_FAILURE_SCENARIOS=()
EXIT_CODE=0
for expected_failure_file in "${EXPECTED_FAILURE_FILES[@]}"; do
PATH_TO_EXPECTED_FAILURE_FILE="${SCRIPT_PATH}/${expected_failure_file}"
EXPECTED_FAILURE_SCENARIOS=($(grep -Eo ${SCENARIO_REGEX} ${PATH_TO_EXPECTED_FAILURE_FILE}))
# get and store only the suites names from EXPECTED_FAILURE_SCENARIOS
EXPECTED_FAILURE_SUITES=()
for scenario in "${EXPECTED_FAILURE_SCENARIOS[@]}"; do
if [[ $scenario =~ \[([a-zA-Z0-9]+) ]]; then
suite="${BASH_REMATCH[1]}"
EXPECTED_FAILURE_SUITES+=("$suite")
fi
done
# also filter the duplicated suites name
EXPECTED_FAILURE_SUITES=($(echo "${EXPECTED_FAILURE_SUITES[@]}" | tr ' ' '\n' | sort | uniq))
# Check the existence of the suite
NON_EXISTING_SCENARIOS=()
FEATURE_PATTERN="[a-zA-Z0-9]+\\.feature:[0-9]+"
for suite in "${EXPECTED_FAILURE_SUITES[@]}"; do
pattern="(\\b${suite}/${FEATURE_PATTERN}\\b)"
if [[ " ${AVAILABLE_SUITES[*]} " != *" $suite "* ]]; then
NON_EXISTING_SCENARIOS+=($(grep -Eo ${pattern} ${PATH_TO_EXPECTED_FAILURE_FILE}))
else
SCENARIOS=($(grep -Eo ${pattern} ${PATH_TO_EXPECTED_FAILURE_FILE} | grep -Eo "${FEATURE_PATTERN}"))
for scenario in "${SCENARIOS[@]}"; do
FEATURE_FILE=$(echo "$scenario" | cut -d':' -f1)
if [[ ! -f "$PATH_TO_SUITES/$suite/$FEATURE_FILE" ]]; then
NON_EXISTING_SCENARIOS+=("$suite/$scenario")
fi
done
fi
done
count="${#NON_EXISTING_SCENARIOS[@]}"
if [ "$count" -gt 0 ]; then
EXIT_CODE=1
log_info "The following test scenarios do not exist anymore:"
log_info "They can be deleted from the '${expected_failure_file}'"
for scenario_path in "${NON_EXISTING_SCENARIOS[@]}"; do
log_error "$scenario_path"
done
else
log_success "All the suites in the expected failure file '${expected_failure_file}' exist!"
fi
log_info "\n"
done
exit "$EXIT_CODE"
@@ -0,0 +1,14 @@
{
"rules": {
"indentation": "error",
"no_repetitive_step_keyword": "error",
"require_step": "error",
"no_trailing_whitespace": "error",
"require_scenario": "error",
"no_then_as_first_step": "error",
"only_given_step_in_background": "error",
"newline_before_scenario": ["error", 2, true],
"lowercase_title": "off",
"no_but_in_given_when": "error"
}
}
+776
View File
@@ -0,0 +1,776 @@
default:
autoload:
"": "%paths.base%/../bootstrap"
suites:
apiSpaces:
paths:
- "%paths.base%/../features/apiSpaces"
context: &common_ldap_suite_context
parameters:
ldapAdminPassword: admin
ldapUsersOU: TestUsers
ldapGroupsOU: TestGroups
ldapInitialUserFilePath: /../config/ldap-users.ldif
contexts:
- FeatureContext: &common_feature_context_params
adminUsername: admin
adminPassword: admin
regularUserPassword: 123456
- SettingsContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
- NotificationContext:
- OCSContext:
- PublicWebDavContext:
- TagContext:
- TUSContext:
- SpacesTUSContext:
- GraphContext:
- OcConfigContext:
- WebDavLockingContext:
- SharingNgContext:
apiSpacesShares:
paths:
- "%paths.base%/../features/apiSpacesShares"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- SpacesContext:
- CapabilitiesContext:
- ChecksumContext:
- FavoritesContext:
- FilesVersionsContext:
- OCSContext:
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- TUSContext:
- SpacesTUSContext:
- ArchiverContext:
- GraphContext:
- OcConfigContext:
- SharingNgContext:
apiContract:
paths:
- "%paths.base%/../features/apiContract"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
- OCSContext:
- PublicWebDavContext:
- SearchContext:
- TUSContext:
- SpacesTUSContext:
- GraphContext:
- SharingNgContext:
- OcConfigContext:
apiArchiver:
paths:
- "%paths.base%/../features/apiArchiver"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- ArchiverContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
- OCSContext:
- PublicWebDavContext:
- GraphContext:
- SharingNgContext:
apiGraph:
paths:
- "%paths.base%/../features/apiGraph"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- GraphContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
- OCSContext:
- PublicWebDavContext:
- TUSContext:
- SpacesTUSContext:
- OcConfigContext:
- TrashbinContext:
- SharingNgContext:
apiGraphUserGroup:
paths:
- "%paths.base%/../features/apiGraphUserGroup"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- GraphContext:
- SpacesContext:
- CapabilitiesContext:
- FilesVersionsContext:
- OCSContext:
- PublicWebDavContext:
- TUSContext:
- SpacesTUSContext:
- OcConfigContext:
- TrashbinContext:
- SharingNgContext:
apiCors:
paths:
- "%paths.base%/../features/apiCors"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- FilesVersionsContext:
- OCSContext:
- GraphContext:
- OcConfigContext:
- SettingsContext:
- TUSContext:
- SpacesTUSContext:
apiDepthInfinity:
paths:
- "%paths.base%/../features/apiDepthInfinity"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- OCSContext:
- GraphContext:
- PublicWebDavContext:
- TrashbinContext:
- OcConfigContext:
- SharingNgContext:
apiAsyncUpload:
paths:
- "%paths.base%/../features/apiAsyncUpload"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- WebDavPropertiesContext:
- FilesVersionsContext:
- OCSContext:
- GraphContext:
- OcConfigContext:
apiNotification:
paths:
- "%paths.base%/../features/apiNotification"
context: *common_ldap_suite_context
contexts:
- NotificationContext:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- OCSContext:
- GraphContext:
- FilesVersionsContext:
- SettingsContext:
- OcConfigContext:
- SharingNgContext:
- OcmContext:
apiAntivirus:
paths:
- "%paths.base%/../features/apiAntivirus"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- NotificationContext:
- SpacesContext:
- OCSContext:
- GraphContext:
- FilesVersionsContext:
- SettingsContext:
- OcConfigContext:
- PublicWebDavContext:
- SharingNgContext:
apiDownloads:
paths:
- "%paths.base%/../features/apiDownloads"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- NotificationContext:
- SpacesContext:
- WebDavPropertiesContext:
- OCSContext:
- GraphContext:
- TrashbinContext:
- FavoritesContext:
- ChecksumContext:
- FilesVersionsContext:
- SettingsContext:
- OcConfigContext:
- PublicWebDavContext:
- ArchiverContext:
- SharingNgContext:
apiSearch1:
paths:
- "%paths.base%/../features/apiSearch1"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- GraphContext:
- SpacesContext:
- PublicWebDavContext:
- SearchContext:
- OCSContext:
- SpacesTUSContext:
- TagContext:
- TUSContext:
- WebDavPropertiesContext:
- SharingNgContext:
apiSearch2:
paths:
- "%paths.base%/../features/apiSearch2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- GraphContext:
- SpacesContext:
- FilesVersionsContext:
- SearchContext:
- OCSContext:
- TrashbinContext:
- SpacesTUSContext:
- TagContext:
- TUSContext:
- SharingNgContext:
apiSearchContent:
paths:
- "%paths.base%/../features/apiSearchContent"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SettingsContext:
- GraphContext:
- SpacesContext:
- PublicWebDavContext:
- SearchContext:
- CapabilitiesContext:
- FilesVersionsContext:
- OCSContext:
- TrashbinContext:
- TagContext:
- SpacesTUSContext:
- TUSContext:
- OcConfigContext:
- SharingNgContext:
- WebDavPropertiesContext:
apiReshare:
paths:
- "%paths.base%/../features/apiReshare"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- OcConfigContext:
- SharingNgContext:
apiSpacesDavOperation:
paths:
- "%paths.base%/../features/apiSpacesDavOperation"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- WebDavPropertiesContext:
- FilesVersionsContext:
- SharingNgContext:
- SettingsContext:
- OcConfigContext:
apiLocks:
paths:
- "%paths.base%/../features/apiLocks"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- WebDavLockingContext:
- PublicWebDavContext:
- SharingNgContext:
- OcConfigContext:
apiSharingNg1:
paths:
- "%paths.base%/../features/apiSharingNg1"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- SharingNgContext:
- OcConfigContext:
- SettingsContext:
apiSharingNg2:
paths:
- "%paths.base%/../features/apiSharingNg2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- SharingNgContext:
- OcConfigContext:
- SettingsContext:
apiSettings:
paths:
- "%paths.base%/../features/apiSettings"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- GraphContext:
- SettingsContext:
apiSharingNgShareInvitation:
paths:
- "%paths.base%/../features/apiSharingNgShareInvitation"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- SharingNgContext:
apiSharingNgLinkSharePermission:
paths:
- "%paths.base%/../features/apiSharingNgLinkSharePermission"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- SharingNgContext:
- PublicWebDavContext:
- OcConfigContext:
apiSharingNgLinkShareRoot:
paths:
- "%paths.base%/../features/apiSharingNgLinkShareRoot"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- SharingNgContext:
- PublicWebDavContext:
- OcConfigContext:
apiOcm:
paths:
- "%paths.base%/../features/apiOcm"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- OcmContext:
- SharingNgContext:
- SpacesContext:
- OcConfigContext:
- NotificationContext:
- SettingsContext:
- SpacesTUSContext:
apiActivities:
paths:
- "%paths.base%/../features/apiActivities"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SharingNgContext:
- GraphContext:
- PublicWebDavContext:
apiCollaboration:
paths:
- "%paths.base%/../features/apiCollaboration"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SharingNgContext:
- CollaborationContext:
- TrashbinContext:
apiServiceAvailability:
paths:
- "%paths.base%/../features/apiServiceAvailability"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- OcConfigContext:
apiAuthApp:
paths:
- "%paths.base%/../features/apiAuthApp"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- AuthAppContext:
- CliContext:
- OcConfigContext:
apiTenancy:
paths:
- "%paths.base%/../features/apiTenancy"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
cliCommands:
paths:
- "%paths.base%/../features/cliCommands"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- CliContext:
- OcConfigContext:
- FilesVersionsContext:
- SharingNgContext:
- SearchContext:
- TagContext:
- TrashbinContext:
- SpacesTUSContext:
collaborativePosix:
paths:
- "%paths.base%/../features/collaborativePosix"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- CliContext:
- OcConfigContext:
- SharingNgContext:
- PublicWebDavContext:
coreApiMain:
paths:
- "%paths.base%/../features/coreApiMain"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- ChecksumContext:
- FilesVersionsContext:
- TrashbinContext:
- CapabilitiesContext:
- SharingNgContext:
coreApiAuth:
paths:
- "%paths.base%/../features/coreApiAuth"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SearchContext:
- PublicWebDavContext:
- WebDavPropertiesContext:
- AuthContext:
coreApiCapabilities:
paths:
- "%paths.base%/../features/coreApiCapabilities"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- CapabilitiesContext:
coreApiShareCreateSpecialToShares1:
paths:
- "%paths.base%/../features/coreApiShareCreateSpecialToShares1"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- TrashbinContext:
- WebDavPropertiesContext:
- SettingsContext:
coreApiShareCreateSpecialToShares2:
paths:
- "%paths.base%/../features/coreApiShareCreateSpecialToShares2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- TrashbinContext:
- WebDavPropertiesContext:
- SettingsContext:
- SharingNgContext:
coreApiSharees:
paths:
- "%paths.base%/../features/coreApiSharees"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- ShareesContext:
- OcConfigContext:
coreApiShareManagementToShares:
paths:
- "%paths.base%/../features/coreApiShareManagementToShares"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SpacesContext:
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- FilesVersionsContext:
- SettingsContext:
- SharingNgContext:
- OcConfigContext:
coreApiShareManagementBasicToShares:
paths:
- "%paths.base%/../features/coreApiShareManagementBasicToShares"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- AuthContext:
- SharingNgContext:
coreApiShareOperationsToShares1:
paths:
- "%paths.base%/../features/coreApiShareOperationsToShares1"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- SettingsContext:
- SharingNgContext:
coreApiShareOperationsToShares2:
paths:
- "%paths.base%/../features/coreApiShareOperationsToShares2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- SharingNgContext:
coreApiSharePublicLink1:
paths:
- "%paths.base%/../features/coreApiSharePublicLink1"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TrashbinContext:
- SharingNgContext:
- WebDavPropertiesContext:
- OcConfigContext:
coreApiSharePublicLink2:
paths:
- "%paths.base%/../features/coreApiSharePublicLink2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- OcConfigContext:
- SharingNgContext:
coreApiShareUpdateToShares:
paths:
- "%paths.base%/../features/coreApiShareUpdateToShares"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TrashbinContext:
- WebDavPropertiesContext:
- SharingNgContext:
coreApiTrashbin:
paths:
- "%paths.base%/../features/coreApiTrashbin"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- TrashbinContext:
- WebDavPropertiesContext:
- SharingNgContext:
coreApiTrashbinRestore:
paths:
- "%paths.base%/../features/coreApiTrashbinRestore"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- TrashbinContext:
- WebDavPropertiesContext:
coreApiVersions:
paths:
- "%paths.base%/../features/coreApiVersions"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- ChecksumContext:
- FilesVersionsContext:
- WebDavPropertiesContext:
- TrashbinContext:
- SharingNgContext:
- OcConfigContext:
coreApiWebdavDelete:
paths:
- "%paths.base%/../features/coreApiWebdavDelete"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SearchContext:
- PublicWebDavContext:
- WebDavPropertiesContext:
- TrashbinContext:
coreApiWebdavLocks:
paths:
- "%paths.base%/../features/coreApiWebdavLocks"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- WebDavLockingContext:
- WebDavPropertiesContext:
coreApiWebdavLocks2:
paths:
- "%paths.base%/../features/coreApiWebdavLocks2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- WebDavLockingContext:
- WebDavPropertiesContext:
coreApiWebdavMove:
paths:
- "%paths.base%/../features/coreApiWebdavMove"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SharingNgContext:
- WebDavPropertiesContext:
- FilesVersionsContext:
- TrashbinContext:
coreApiWebdavOperations:
paths:
- "%paths.base%/../features/coreApiWebdavOperations"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SearchContext:
- PublicWebDavContext:
- SharingNgContext:
- WebDavPropertiesContext:
- TrashbinContext:
- OcConfigContext:
coreApiWebdavPreviews:
paths:
- "%paths.base%/../features/coreApiWebdavPreviews"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SharingNgContext:
- WebDavPropertiesContext:
- OcConfigContext:
coreApiWebdavProperties:
paths:
- "%paths.base%/../features/coreApiWebdavProperties"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SharingNgContext:
- WebDavPropertiesContext:
- OcConfigContext:
coreApiWebdavUpload:
paths:
- "%paths.base%/../features/coreApiWebdavUpload"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- WebDavPropertiesContext:
- SharingNgContext:
coreApiWebdavUploadTUS:
paths:
- "%paths.base%/../features/coreApiWebdavUploadTUS"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- PublicWebDavContext:
- TUSContext:
- FilesVersionsContext:
- ChecksumContext:
- SharingNgContext:
coreApiWebdavEtagPropagation1:
paths:
- "%paths.base%/../features/coreApiWebdavEtagPropagation1"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- TrashbinContext:
- PublicWebDavContext:
- FilesVersionsContext:
- SharingNgContext:
- WebDavPropertiesContext:
coreApiWebdavEtagPropagation2:
paths:
- "%paths.base%/../features/coreApiWebdavEtagPropagation2"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- TrashbinContext:
- PublicWebDavContext:
- FilesVersionsContext:
- SharingNgContext:
- WebDavPropertiesContext:
- OcConfigContext:
coreApiWebdavCopyCreate:
paths:
- "%paths.base%/../features/coreApiWebdavCopyCreate"
context: *common_ldap_suite_context
contexts:
- FeatureContext: *common_feature_context_params
- SharingNgContext:
- OcConfigContext:
extensions:
rdx\behatvars\BehatVariablesExtension: ~
Cjm\Behat\StepThroughExtension: ~
@@ -0,0 +1,9 @@
dn: ou=TestUsers,dc=example,dc=org
objectclass: top
objectclass: organizationalUnit
ou: TestUsers
dn: ou=TestGroups,dc=example,dc=org
objectclass: top
objectclass: organizationalUnit
ou: TestGroups
+165
View File
@@ -0,0 +1,165 @@
.ONESHELL:
SHELL := bash
# define standard colors
BLACK := $(shell tput -Txterm setaf 0)
RED := $(shell tput -Txterm setaf 1)
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
BLUE := $(shell tput -Txterm setaf 4)
PURPLE := $(shell tput -Txterm setaf 5)
CYAN := $(shell tput -Txterm setaf 6)
WHITE := $(shell tput -Txterm setaf 7)
RESET := $(shell tput -Txterm sgr0)
COMPOSE_FILE := src/qsfera-base.yml
# run tests with ocwrapper by default
WITH_WRAPPER ?= true
OC_WRAPPER := ../../ocwrapper/bin/ocwrapper
# enable tika for full text extraction
ifeq ($(START_TIKA),true)
COMPOSE_FILE := src/tika.yml:$(COMPOSE_FILE)
export SEARCH_EXTRACTOR_TYPE := tika
else
export SEARCH_EXTRACTOR_TYPE := basic
endif
# enable email server
ifeq ($(START_EMAIL),true)
COMPOSE_FILE := src/email.yml:$(COMPOSE_FILE)
export OC_ADD_RUN_SERVICES := notifications
endif
# enable antivirus
ifeq ($(START_ANTIVIRUS),true)
COMPOSE_FILE := src/antivirus.yml:$(COMPOSE_FILE)
export OC_ADD_RUN_SERVICES := $(OC_ADD_RUN_SERVICES) antivirus
export POSTPROCESSING_STEPS := virusscan
endif
# enable wopi services
ifeq ($(ENABLE_WOPI),true)
COMPOSE_FILE := $(COMPOSE_FILE):src/wopi.yml
endif
# default to posix
STORAGE_DRIVER ?= posix
ifeq ($(STORAGE_DRIVER),posix)
# posix requires a additional driver config
COMPOSE_FILE := $(COMPOSE_FILE):src/posix.yml
else ifeq ($(STORAGE_DRIVER),decomposeds3)
COMPOSE_FILE := src/ceph.yml:$(COMPOSE_FILE)
endif
# use latest as default tag if OC_IMAGE is provided but no tag is set
ifneq ($(strip $(OC_IMAGE)),)
ifeq ($(strip $(OC_IMAGE_TAG)),)
OC_IMAGE_TAG := latest
endif
endif
COMPOSE_PROJECT_NAME := qsfera-acceptance-tests
# Export variables for sub-make calls
export COMPOSE_PROJECT_NAME
export COMPOSE_FILE
export OC_IMAGE
export OC_IMAGE_TAG
# test configurations
export STORAGE_DRIVER
export WITH_WRAPPER
export BEHAT_SUITE
export BEHAT_FEATURE
export TEST_SERVER_URL
export USE_BEARER_TOKEN
## make definition
.PHONY: help
help:
@echo -e "Test suites: ${CYAN}https://github.com/qsfera/server/tree/main/tests/acceptance/features${RESET}"
@echo -e "Testing docs: ${CYAN}https://github.com/qsfera/server/tree/main/tests/README.md${RESET}"
@echo
@echo "Available commands (targets):"
@echo -e " ${GREEN}run-api-tests\t\t${RESET}Build dev image, start services and run the tests."
@echo -e " ${GREEN}start-services\t${RESET}Start service containers."
@echo -e " ${GREEN}run-test-only\t\t${RESET}Run the tests only."
@echo -e " ${GREEN}show-test-logs\t${RESET}Show the test logs."
@echo -e " ${GREEN}ps\t\t\t${RESET}Show the running containers."
@echo
@echo -e " ${YELLOW}clean-dev-image\t${RESET}Delete the docker image built during acceptance tests."
@echo -e " ${YELLOW}clean-containers\t${RESET}Delete the docker containers and volumes."
@echo -e " ${YELLOW}clean-tests\t\t${RESET}Delete test dependencies and results."
@echo -e " ${YELLOW}clean-all\t\t${RESET}Clean all resources: images, containers, volumes, test files."
@echo
@echo "Available environment variables:"
@echo -e " ${PURPLE}OC_IMAGE\t\t${RESET}${CYAN}[image_repo]${RESET} КуСфера image to use. If provided, the dev image build is skipped."
@echo -e " ${PURPLE}OC_IMAGE_TAG\t\t${RESET}${CYAN}[image_tag]${RESET} КуСфера image tag to use. If provided, the dev image build is skipped."
@echo -e " ${PURPLE}WITH_WRAPPER\t\t${RESET}${CYAN}[true|false]${RESET} Start КуСфера server using ocwrapper. Default: ${YELLOW}true${RESET}"
@echo -e " ${PURPLE}STORAGE_DRIVER\t${RESET}${CYAN}[posix|decomposed|decomposeds3]${RESET} Storage driver to use. Default: ${YELLOW}posix${RESET}"
@echo -e " ${PURPLE}BEHAT_FEATURE\t\t${RESET}${RESET}${CYAN}[path]${RESET} Path to a feature file. Example: ${YELLOW}tests/acceptance/features/apiGraph/changeRole.feature${RESET}"
@echo -e " ${PURPLE}BEHAT_SUITE\t\t${RESET}${RESET}${CYAN}[suite_name]${RESET} Test suite to run. Example: ${YELLOW}apiGraph${RESET}"
@echo -e " ${PURPLE}TEST_SERVER_URL\t${RESET}${CYAN}[url]${RESET} URL of the КуСфера server to test against."
@echo -e " ${PURPLE}USE_BEARER_TOKEN\t${RESET}${CYAN}[true|false]${RESET} Use a bearer token for authentication. Default: ${YELLOW}false${RESET}"
@echo
@echo -e "Example usage:"
@echo -e " ${PURPLE}WITH_WRAPPER${RESET}=${YELLOW}false${RESET} \\"
@echo -e " ${PURPLE}STORAGE_DRIVER${RESET}=${YELLOW}posix${RESET} \\"
@echo -e " ${PURPLE}BEHAT_FEATURE${RESET}=${YELLOW}tests/acceptance/features/apiGraph/changeRole.feature${RESET} \\"
@echo -e " make ${GREEN}run-api-tests${RESET}"
.PHONY: run-api-tests
run-api-tests: $(OC_WRAPPER) build-dev-image clean-containers
@echo "${BLUE}[INFO]${RESET} Compose project: ${YELLOW}$(COMPOSE_PROJECT_NAME)${RESET}"
@echo "${BLUE}[INFO]${RESET} Compose file: ${YELLOW}$(COMPOSE_FILE)${RESET}"
@echo "${BLUE}[INFO]${RESET} Using storage driver: ${YELLOW}$(STORAGE_DRIVER)${RESET}"
# force use local server when using this command
export TEST_SERVER_URL=https://qsfera-server:9200
$(MAKE) --no-print-directory start-services
$(MAKE) --no-print-directory run-test-only
.PHONY: start-services
start-services: $(OC_WRAPPER) ## start services
docker compose up -d --build --force-recreate
.PHONY: run-test-only
run-test-only:
docker compose -f src/acceptance.yml up
.PHONY: show-test-logs
show-test-logs: ## show test logs
docker compose logs --no-log-prefix -f acceptance-tests | less
.PHONY: ps
ps: ## show running containers
docker compose ps
$(OC_WRAPPER):
@if [ "$(WITH_WRAPPER)" == "true" ]; then \
$(MAKE) --no-print-directory -C ../../ocwrapper build \
; fi;
.PHONY: build-dev-image
build-dev-image:
@if [ -z "$(OC_IMAGE)" ] && [ -z "$(OC_IMAGE_TAG)" ]; then \
$(MAKE) --no-print-directory -C ../../../qsfera dev-docker \
; fi;
.PHONY: clean-dev-image
clean-dev-image: ## clean docker image built during acceptance tests
@docker image rm qsfera/qsfera:dev || true
.PHONY: clean-containers
clean-containers: ## clean docker containers created during acceptance tests
docker compose down --remove-orphans -v
.PHONY: clean-tests
clean-tests:
@$(MAKE) --no-print-directory -C ../../../. clean-tests
.PHONY: clean
clean-all: clean-containers clean-dev-image clean-tests ## clean all
@@ -0,0 +1,22 @@
services:
acceptance-tests:
image: opencloudeu/php-alpine-ci:8.4
working_dir: /woodpecker/src/github.com/qsfera/server
command: /bin/bash /test/run-tests.sh
environment:
OC_ROOT: /woodpecker/src/github.com/qsfera/server
TEST_SERVER_URL: ${TEST_SERVER_URL:-https://qsfera-server:9200}
OC_WRAPPER_URL: http://qsfera-server:5200
STORAGE_DRIVER: ${STORAGE_DRIVER:-posix}
BEHAT_SUITE: ${BEHAT_SUITE:-}
BEHAT_FEATURE: ${BEHAT_FEATURE:-}
USE_BEARER_TOKEN: ${USE_BEARER_TOKEN:-false}
# email
EMAIL_HOST: email
EMAIL_PORT: 9000
env_file:
- ../../../../.woodpecker.env
volumes:
- ./run-tests.sh:/test/run-tests.sh
- ../../../../:/woodpecker/src/github.com/qsfera/server
- ../../../../vendor-bin/behat/composer.json:/tmp/vendor-bin/behat/composer.json
@@ -0,0 +1,9 @@
services:
start_antivirus:
image: dadarek/wait-for-dependencies
depends_on:
- clamav
command: clamav:3310
clamav:
image: opencloudeu/clamav-ci:latest
@@ -0,0 +1,17 @@
services:
start_ceph:
image: dadarek/wait-for-dependencies
depends_on:
- ceph
command: ceph:5000
ceph:
image: ceph/daemon
command: demo
environment:
MON_IP: 127.0.0.1
CEPH_PUBLIC_NETWORK: 0.0.0.0/0
CEPH_DEMO_UID: test-user
CEPH_DEMO_ACCESS_KEY: test
CEPH_DEMO_SECRET_KEY: test
CEPH_DEMO_BUCKET: test
RGW_NAME: ceph
@@ -0,0 +1,10 @@
services:
start_email:
image: dadarek/wait-for-dependencies
depends_on:
- email
command: email:9000
email:
image: inbucket/inbucket
ports:
- '9000:9000'
@@ -0,0 +1,10 @@
#!/bin/bash
set -e
mkdir -p /var/www/onlyoffice/Data/certs
cd /var/www/onlyoffice/Data/certs
openssl req -x509 -newkey rsa:4096 -keyout onlyoffice.key -out onlyoffice.crt -sha256 -days 365 -batch -nodes
chmod 400 /var/www/onlyoffice/Data/certs/onlyoffice.key
/app/ds/run-document-server.sh
@@ -0,0 +1,7 @@
---
services:
qsfera-server:
environment:
# posix requires a shared cache store
STORAGE_USERS_ID_CACHE_STORE: "nats-js-kv"
STORAGE_USERS_POSIX_WATCH_FS: "true"
@@ -0,0 +1,58 @@
services:
qsfera-server:
image: ${OC_IMAGE:-qsfera/qsfera}:${OC_IMAGE_TAG:-dev}
entrypoint: ["/bin/sh", "/usr/bin/serve-qsfera.sh"]
user: root
environment:
WITH_WRAPPER: ${WITH_WRAPPER:-true}
OC_URL: "https://qsfera-server:9200"
STORAGE_USERS_DRIVER: ${STORAGE_DRIVER:-posix}
STORAGE_USERS_POSIX_WATCH_FS: "true"
STORAGE_USERS_DRIVER_LOCAL_ROOT: /srv/app/tmp/qsfera/local/root
STORAGE_USERS_DRIVER_OC_ROOT: /srv/app/tmp/qsfera/storage/users
STORAGE_SYSTEM_DRIVER_OC_ROOT: /srv/app/tmp/qsfera/storage/metadata
SHARING_USER_JSON_FILE: /srv/app/tmp/qsfera/shares.json
PROXY_ENABLE_BASIC_AUTH: "true"
WEB_UI_CONFIG_FILE: /woodpecker/src/github.com/qsfera/server/tests/config/woodpecker/qsfera-config.json
OC_INSECURE: "true"
IDM_CREATE_DEMO_USERS: "true"
IDM_ADMIN_PASSWORD: "admin"
FRONTEND_SEARCH_MIN_LENGTH: "2"
OC_ADD_RUN_SERVICES: ${OC_ADD_RUN_SERVICES:-}
PROXY_HTTP_ADDR: "0.0.0.0:9200"
# decomposeds3 specific settings
STORAGE_USERS_DECOMPOSEDS3_ENDPOINT: http://ceph:8080
STORAGE_USERS_DECOMPOSEDS3_REGION: default
STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY: test
STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: test
STORAGE_USERS_DECOMPOSEDS3_BUCKET: test
# email
NOTIFICATIONS_SMTP_HOST: email
NOTIFICATIONS_SMTP_PORT: 2500
NOTIFICATIONS_SMTP_INSECURE: "true"
NOTIFICATIONS_SMTP_SENDER: "КуСфера <noreply@example.com>"
NOTIFICATIONS_DEBUG_ADDR: 0.0.0.0:9174
# antivirus
ANTIVIRUS_SCANNER_TYPE: "clamav"
ANTIVIRUS_CLAMAV_SOCKET: tcp://clamav:3310
# postprocessing step
POSTPROCESSING_STEPS: ${POSTPROCESSING_STEPS:-}
# tika
SEARCH_EXTRACTOR_TYPE: ${SEARCH_EXTRACTOR_TYPE:-basic}
SEARCH_EXTRACTOR_TIKA_TIKA_URL: "http://tika:9998"
SEARCH_EXTRACTOR_CS3SOURCE_INSECURE: "true"
# fonts map for txt thumbnails (including unicode support)
THUMBNAILS_TXT_FONTMAP_FILE: "/woodpecker/src/github.com/qsfera/server/tests/config/drone/fontsMap.json"
ports:
- "9200:9200"
- "5200:5200" ## ocwrapper
- "9174:9174" ## notifications debug
volumes:
- ../../../config:/woodpecker/src/github.com/qsfera/server/tests/config
- ../../../ocwrapper/bin/ocwrapper:/usr/bin/ocwrapper
- ./serve-qsfera.sh:/usr/bin/serve-qsfera.sh
@@ -0,0 +1,42 @@
#!/bin/bash
mkdir -p "${OC_ROOT}/vendor-bin/behat"
if [ ! -f "${OC_ROOT}/vendor-bin/behat/composer.json" ]; then
cp /tmp/vendor-bin/behat/composer.json "${OC_ROOT}/vendor-bin/behat/composer.json"
fi
git config --global advice.detachedHead false
## CONFIGURE TEST
BEHAT_FILTER_TAGS='~@skip'
EXPECTED_FAILURES_FILE=''
if [ "$STORAGE_DRIVER" = "posix" ]; then
BEHAT_FILTER_TAGS+='&&~@skipOnQsfera-posix-Storage'
EXPECTED_FAILURES_FILE="${OC_ROOT}/tests/acceptance/expected-failures-posix-storage.md"
elif [ "$STORAGE_DRIVER" = "decomposed" ]; then
BEHAT_FILTER_TAGS+='&&~@skipOnQsfera-decomposed-Storage'
EXPECTED_FAILURES_FILE="${OC_ROOT}/tests/acceptance/expected-failures-decomposed-storage.md"
fi
export BEHAT_FILTER_TAGS
export EXPECTED_FAILURES_FILE
if [ -n "$BEHAT_FEATURE" ]; then
export BEHAT_FEATURE
echo "[INFO] Running feature: $BEHAT_FEATURE"
# allow running without filters if its a feature
unset BEHAT_FILTER_TAGS
unset BEHAT_SUITE
unset EXPECTED_FAILURES_FILE
elif [ -n "$BEHAT_SUITE" ]; then
export BEHAT_SUITE
echo "[INFO] Running suite: $BEHAT_SUITE"
unset BEHAT_FEATURE
fi
## RUN TEST
sleep 10
make -C "$OC_ROOT" test-acceptance-api
chmod -R 777 "${OC_ROOT}/vendor-bin/"*"/vendor" "${OC_ROOT}/vendor-bin/"*"/composer.lock" "${OC_ROOT}/tests/acceptance/output" 2>/dev/null || true
@@ -0,0 +1,45 @@
#!/bin/sh
set -xe
if [ -z "$TEST_GROUP" ]; then
echo "TEST_GROUP not set"
exit 1
fi
echo "Waiting for collaboration WOPI endpoint..."
until curl -s http://collaboration:9304 >/dev/null; do
echo "Waiting for collaboration WOPI endpoint..."
sleep 2
done
echo "Collaboration is up"
if [ -z "$OC_URL" ]; then
OC_URL="https://qsfera-server:9200"
fi
curl -vk -X DELETE "$OC_URL/remote.php/webdav/test.wopitest" -u admin:admin
curl -vk -X PUT "$OC_URL/remote.php/webdav/test.wopitest" -u admin:admin -D headers.txt
cat headers.txt
FILE_ID="$(cat headers.txt | sed -n -e 's/^.*oc-fileid: //Ip')"
export FILE_ID
URL="$OC_URL/app/open?app_name=FakeOffice&file_id=$FILE_ID"
URL="$(echo "$URL" | tr -d '[:cntrl:]')"
export URL
curl -vk -X POST "$URL" -u admin:admin > open.json
cat open.json
cat open.json | jq .form_parameters.access_token | tr -d '"' > accesstoken
cat open.json | jq .form_parameters.access_token_ttl | tr -d '"' > accesstokenttl
WOPI_FILE_ID="$(cat open.json | jq .app_url | sed -n -e 's/^.*files%2F//p' | tr -d '"')"
echo "http://collaboration:9300/wopi/files/$WOPI_FILE_ID" > wopisrc
WOPI_TOKEN=$(cat accesstoken)
export WOPI_TOKEN
WOPI_TTL=$(cat accesstokenttl)
export WOPI_TTL
WOPI_SRC=$(cat wopisrc)
export WOPI_SRC
/app/Microsoft.Office.WopiValidator -s -t "$WOPI_TOKEN" -w "$WOPI_SRC" -l "$WOPI_TTL" --testgroup $TEST_GROUP
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
# init КуСфера
qsfera init
if [ "$WITH_WRAPPER" = "true" ]; then
ocwrapper serve --bin=qsfera
else
qsfera server
fi
@@ -0,0 +1,8 @@
services:
tika-service:
image: dadarek/wait-for-dependencies
depends_on:
- tika
command: tika:9998
tika:
image: apache/tika:3.2.3.0-full
@@ -0,0 +1,86 @@
services:
fakeoffice:
image: owncloudci/alpine:latest
entrypoint: /bin/sh
command:
[
"-c",
"while true; do echo -e \"HTTP/1.1 200 OK\n\n$(cat /hosting-discovery.xml)\" | nc -l -k -p 8080; done",
]
ports:
- 8080:8080
extra_hosts:
- qsfera.local:${DOCKER_HOST:-host-gateway}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080"]
volumes:
- ./../../../config/woodpecker/hosting-discovery.xml:/hosting-discovery.xml
qsfera:
image: qsfera/qsfera:dev
container_name: qsfera-server
ports:
- 9200:9200
entrypoint: /bin/sh
command: ["-c", "qsfera init || true; sleep 10; qsfera server"]
environment:
OC_URL: https://qsfera-server:9200
OC_CONFIG_DIR: /etc/qsfera
STORAGE_USERS_DRIVER: posix
PROXY_ENABLE_BASIC_AUTH: true
OC_LOG_LEVEL: error
OC_LOG_COLOR: false
OC_INSECURE: true
IDM_ADMIN_PASSWORD: admin
GATEWAY_GRPC_ADDR: 0.0.0.0:9142
NATS_NATS_HOST: 0.0.0.0
NATS_NATS_PORT: 9233
volumes:
- config:/etc/qsfera
depends_on:
fakeoffice:
condition: service_healthy
collaboration:
image: qsfera/qsfera:dev
restart: unless-stopped
ports:
- 9300:9300
entrypoint:
- /bin/sh
command: ["-c", "qsfera collaboration server"]
environment:
OC_CONFIG_DIR: /etc/qsfera
MICRO_REGISTRY: nats-js-kv
MICRO_REGISTRY_ADDRESS: qsfera:9233
COLLABORATION_LOG_LEVEL: info
COLLABORATION_GRPC_ADDR: 0.0.0.0:9301
COLLABORATION_HTTP_ADDR: 0.0.0.0:9300
COLLABORATION_DEBUG_ADDR: 0.0.0.0:9304
COLLABORATION_APP_PROOF_DISABLE: true
COLLABORATION_APP_INSECURE: true
COLLABORATION_CS3API_DATAGATEWAY_INSECURE: true
COLLABORATION_WOPI_SECRET: some-wopi-secret
COLLABORATION_SERVICE_NAME: collaboration-fakeoffice
COLLABORATION_APP_NAME: FakeOffice
COLLABORATION_APP_PRODUCT: Microsoft
COLLABORATION_APP_ADDR: http://fakeoffice:8080
COLLABORATION_WOPI_SRC: http://collaboration:9300
volumes:
- config:/etc/qsfera
depends_on:
- qsfera
wopi-validator:
image: ${WOPI_VALIDATOR_IMAGE:-opencloudeu/wopi-validator-ci}
volumes:
- ./run-wopi-validator.sh:/app/run-wopi-validator.sh
environment:
TEST_GROUP: ${TEST_GROUP:-PutRelativeFile}
entrypoint: /app/run-wopi-validator.sh
depends_on:
- collaboration
restart: "on-failure"
volumes:
config:
+105
View File
@@ -0,0 +1,105 @@
x-common_config: &common_config
image: qsfera/qsfera:dev
restart: unless-stopped
entrypoint: /bin/sh
command: ["-c", "qsfera collaboration server"]
user: root
x-common_env: &common_env
OC_CONFIG_DIR: /etc/qsfera
MICRO_REGISTRY: nats-js-kv
MICRO_REGISTRY_ADDRESS: qsfera-server:9233
COLLABORATION_LOG_LEVEL: info
COLLABORATION_GRPC_ADDR: 0.0.0.0:9301
COLLABORATION_HTTP_ADDR: 0.0.0.0:9300
COLLABORATION_DEBUG_ADDR: 0.0.0.0:9304
COLLABORATION_APP_PROOF_DISABLE: true
COLLABORATION_APP_INSECURE: true
COLLABORATION_CS3API_DATAGATEWAY_INSECURE: true
COLLABORATION_WOPI_SECRET: some-wopi-secret
x-config_volume: &config_volume
- config:/etc/qsfera
x-depends_on: &depends_on
- qsfera-server
services:
qsfera-server:
environment:
OC_CONFIG_DIR: /etc/qsfera
GATEWAY_GRPC_ADDR: 0.0.0.0:9142
NATS_NATS_HOST: 0.0.0.0
NATS_NATS_PORT: 9233
volumes: *config_volume
fakeoffice:
image: alpine:latest
entrypoint: /bin/sh
command:
[
"-c",
"while true; do echo -e \"HTTP/1.1 200 OK\n\n$(cat /fakeoffice-discovery.xml)\" | nc -l -k -p 8080; done",
]
healthcheck:
test: ["CMD", "curl", "-f", "http://fakeoffice:8080"]
volumes:
- ./../../../config/woodpecker/hosting-discovery.xml:/fakeoffice-discovery.xml
collabora:
image: collabora/code:24.04.5.1.1
environment:
DONT_GEN_SSL_CERT: set
extra_params: --o:ssl.enable=true --o:ssl.termination=true --o:welcome.enable=false --o:net.frame_ancestors=https://qsfera-server:9200
entrypoint: /bin/sh
command: ["-c", "coolconfig generate-proof-key; /start-collabora-online.sh"]
onlyoffice:
image: onlyoffice/documentserver:7.5.1
environment:
WOPI_ENABLED: true
USE_UNAUTHORIZED_STORAGE: true
entrypoint: bash /entrypoint.sh
volumes:
- ./onlyoffice-entrypoint.sh:/entrypoint.sh
collaboration-fakeoffice:
<<: *common_config
environment:
<<: *common_env
COLLABORATION_SERVICE_NAME: collaboration-fakeoffice
COLLABORATION_APP_NAME: FakeOffice
COLLABORATION_APP_PRODUCT: Microsoft
COLLABORATION_APP_ADDR: http://fakeoffice:8080
COLLABORATION_WOPI_SRC: http://collaboration-fakeoffice:9300
volumes: *config_volume
depends_on: *depends_on
collaboration-collabora:
<<: *common_config
environment:
<<: *common_env
COLLABORATION_SERVICE_NAME: collaboration-collabora
COLLABORATION_APP_NAME: Collabora
COLLABORATION_APP_PRODUCT: Collabora
COLLABORATION_APP_ADDR: https://collabora:9980
COLLABORATION_APP_ICON: https://collabora:9980/favicon.ico
COLLABORATION_WOPI_SRC: http://collaboration-collabora:9300
volumes: *config_volume
depends_on: *depends_on
collaboration-onlyoffice:
<<: *common_config
environment:
<<: *common_env
COLLABORATION_SERVICE_NAME: collaboration-onlyoffice
COLLABORATION_APP_NAME: OnlyOffice
COLLABORATION_APP_PRODUCT: OnlyOffice
COLLABORATION_APP_ADDR: https://onlyoffice
COLLABORATION_APP_ICON: https://onlyoffice/web-apps/apps/documenteditor/main/resources/img/favicon.ico
COLLABORATION_WOPI_SRC: http://collaboration-onlyoffice:9300
volumes: *config_volume
depends_on: *depends_on
volumes:
config:
@@ -0,0 +1,357 @@
## Scenarios from КуСфера API tests that are expected to fail with decomposed storage
#### [Downloading the archive of the resource (files | folder) using resource path is not possible](https://github.com/owncloud/ocis/issues/4637)
- [apiArchiver/downloadByPath.feature:25](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L25)
- [apiArchiver/downloadByPath.feature:26](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L26)
- [apiArchiver/downloadByPath.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L43)
- [apiArchiver/downloadByPath.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L44)
- [apiArchiver/downloadByPath.feature:47](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L47)
- [apiArchiver/downloadByPath.feature:73](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L73)
- [apiArchiver/downloadByPath.feature:171](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L171)
- [apiArchiver/downloadByPath.feature:172](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L172)
#### [PATCH request for TUS upload with wrong checksum gives incorrect response](https://github.com/owncloud/ocis/issues/1755)
- [apiSpacesShares/shareUploadTUS.feature:283](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L283)
- [apiSpacesShares/shareUploadTUS.feature:303](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L303)
- [apiSpacesShares/shareUploadTUS.feature:384](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L384)
#### [Settings service user can list other peoples assignments](https://github.com/owncloud/ocis/issues/5032)
- [apiGraph/getAssignedRole.feature:31](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L31)
- [apiGraph/getAssignedRole.feature:32](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L32)
- [apiGraph/getAssignedRole.feature:33](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L33)
#### [A User can get information of another user with Graph API](https://github.com/owncloud/ocis/issues/5125)
- [apiGraphUserGroup/getUser.feature:84](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L84)
- [apiGraphUserGroup/getUser.feature:85](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L85)
- [apiGraphUserGroup/getUser.feature:86](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L86)
- [apiGraphUserGroup/getUser.feature:628](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L628)
- [apiGraphUserGroup/getUser.feature:629](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L629)
- [apiGraphUserGroup/getUser.feature:630](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L630)
- [apiGraphUserGroup/getUser.feature:645](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L645)
- [apiGraphUserGroup/getUser.feature:646](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L646)
- [apiGraphUserGroup/getUser.feature:647](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L647)
#### [Normal user can get expanded members information of a group](https://github.com/owncloud/ocis/issues/5604)
- [apiGraphUserGroup/getGroup.feature:399](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L399)
- [apiGraphUserGroup/getGroup.feature:400](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L400)
- [apiGraphUserGroup/getGroup.feature:401](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L401)
- [apiGraphUserGroup/getGroup.feature:460](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L460)
- [apiGraphUserGroup/getGroup.feature:461](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L461)
- [apiGraphUserGroup/getGroup.feature:462](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L462)
- [apiGraphUserGroup/getGroup.feature:508](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L508)
- [apiGraphUserGroup/getGroup.feature:509](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L509)
- [apiGraphUserGroup/getGroup.feature:510](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L510)
#### [Same users can be added in a group multiple time](https://github.com/owncloud/ocis/issues/5702)
- [apiGraphUserGroup/addUserToGroup.feature:295](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L295)
#### [Users are added in a group with wrong host in host-part of user](https://github.com/owncloud/ocis/issues/5871)
- [apiGraphUserGroup/addUserToGroup.feature:379](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L379)
- [apiGraphUserGroup/addUserToGroup.feature:393](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L393)
#### [Adding the same user as multiple members in a single request results in listing the same user twice in the group](https://github.com/owncloud/ocis/issues/5855)
- [apiGraphUserGroup/addUserToGroup.feature:430](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L430)
#### [Shared file locking is not possible using different path](https://github.com/owncloud/ocis/issues/7599)
- [apiLocks/lockFiles.feature:185](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L185)
- [apiLocks/lockFiles.feature:186](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L186)
- [apiLocks/lockFiles.feature:187](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L187)
- [apiLocks/lockFiles.feature:309](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L309)
- [apiLocks/lockFiles.feature:310](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L310)
- [apiLocks/lockFiles.feature:311](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L311)
- [apiLocks/lockFiles.feature:364](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L364)
- [apiLocks/lockFiles.feature:365](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L365)
- [apiLocks/lockFiles.feature:366](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L366)
- [apiLocks/lockFiles.feature:367](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L367)
- [apiLocks/lockFiles.feature:368](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L368)
- [apiLocks/lockFiles.feature:369](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L369)
- [apiLocks/lockFiles.feature:399](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L399)
- [apiLocks/lockFiles.feature:400](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L400)
- [apiLocks/lockFiles.feature:401](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L401)
- [apiLocks/lockFiles.feature:402](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L402)
- [apiLocks/lockFiles.feature:403](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L403)
- [apiLocks/lockFiles.feature:404](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L404)
- [apiLocks/unlockFiles.feature:62](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L62)
- [apiLocks/unlockFiles.feature:63](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L63)
- [apiLocks/unlockFiles.feature:64](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L64)
- [apiLocks/unlockFiles.feature:171](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L171)
- [apiLocks/unlockFiles.feature:172](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L172)
- [apiLocks/unlockFiles.feature:173](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L173)
- [apiLocks/unlockFiles.feature:174](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L174)
- [apiLocks/unlockFiles.feature:175](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L175)
- [apiLocks/unlockFiles.feature:176](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L176)
- [apiLocks/unlockFiles.feature:199](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L199)
- [apiLocks/unlockFiles.feature:200](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L200)
- [apiLocks/unlockFiles.feature:201](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L201)
- [apiLocks/unlockFiles.feature:202](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L202)
- [apiLocks/unlockFiles.feature:203](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L203)
- [apiLocks/unlockFiles.feature:204](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L204)
- [apiLocks/unlockFiles.feature:227](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L227)
- [apiLocks/unlockFiles.feature:228](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L228)
- [apiLocks/unlockFiles.feature:229](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L229)
- [apiLocks/unlockFiles.feature:230](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L230)
- [apiLocks/unlockFiles.feature:231](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L231)
- [apiLocks/unlockFiles.feature:232](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L232)
#### [Folders can be locked and locking works partially](https://github.com/owncloud/ocis/issues/7641)
- [apiLocks/lockFiles.feature:443](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L443)
- [apiLocks/lockFiles.feature:444](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L444)
- [apiLocks/lockFiles.feature:445](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L445)
- [apiLocks/lockFiles.feature:446](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L446)
- [apiLocks/lockFiles.feature:447](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L447)
- [apiLocks/lockFiles.feature:448](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L448)
- [apiLocks/lockFiles.feature:417](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L417)
- [apiLocks/lockFiles.feature:418](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L418)
- [apiLocks/lockFiles.feature:419](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L419)
- [apiLocks/lockFiles.feature:420](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L420)
- [apiLocks/lockFiles.feature:421](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L421)
- [apiLocks/lockFiles.feature:422](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L422)
#### [Anonymous users can unlock a file shared to them through a public link if they get the lock token](https://github.com/owncloud/ocis/issues/7761)
- [apiLocks/unlockFiles.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L42)
- [apiLocks/unlockFiles.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L43)
- [apiLocks/unlockFiles.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L44)
- [apiLocks/unlockFiles.feature:45](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L45)
- [apiLocks/unlockFiles.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L46)
- [apiLocks/unlockFiles.feature:47](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L47)
#### [Trying to unlock a shared file with sharer's lock token gives 500](https://github.com/owncloud/ocis/issues/7767)
- [apiLocks/unlockFiles.feature:115](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L115)
- [apiLocks/unlockFiles.feature:116](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L116)
- [apiLocks/unlockFiles.feature:117](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L117)
- [apiLocks/unlockFiles.feature:118](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L118)
- [apiLocks/unlockFiles.feature:119](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L119)
- [apiLocks/unlockFiles.feature:120](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L120)
- [apiLocks/unlockFiles.feature:143](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L143)
- [apiLocks/unlockFiles.feature:144](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L144)
- [apiLocks/unlockFiles.feature:145](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L145)
- [apiLocks/unlockFiles.feature:146](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L146)
- [apiLocks/unlockFiles.feature:147](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L147)
- [apiLocks/unlockFiles.feature:148](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L148)
#### [Anonymous user trying lock a file shared to them through a public link gives 405](https://github.com/owncloud/ocis/issues/7790)
- [apiLocks/lockFiles.feature:532](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L532)
- [apiLocks/lockFiles.feature:533](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L533)
- [apiLocks/lockFiles.feature:534](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L534)
- [apiLocks/lockFiles.feature:535](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L535)
- [apiLocks/lockFiles.feature:554](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L554)
- [apiLocks/lockFiles.feature:555](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L555)
- [apiLocks/lockFiles.feature:556](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L556)
- [apiLocks/lockFiles.feature:557](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L557)
#### [sharee (editor role) MOVE a file by file-id into shared sub-folder returns 502](https://github.com/owncloud/ocis/issues/7617)
- [apiSpacesDavOperation/moveByFileId.feature:368](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L368)
- [apiSpacesDavOperation/moveByFileId.feature:591](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L591)
#### [MOVE a file into same folder with same name returns 404 instead of 403](https://github.com/owncloud/ocis/issues/1976)
- [apiSpacesShares/moveSpaces.feature:69](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L69)
- [apiSpacesShares/moveSpaces.feature:70](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L70)
- [apiSpacesShares/moveSpaces.feature:416](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L416)
- [apiSpacesDavOperation/moveByFileId.feature:61](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L61)
- [apiSpacesDavOperation/moveByFileId.feature:174](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L174)
- [apiSpacesDavOperation/moveByFileId.feature:175](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L175)
- [apiSpacesDavOperation/moveByFileId.feature:176](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L176)
- [apiSpacesDavOperation/moveByFileId.feature:393](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L393)
#### [OCM. admin cannot get federated users if he hasn't connection with them ](https://github.com/owncloud/ocis/issues/9829)
- [apiOcm/searchFederationUsers.feature:429](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/searchFederationUsers.feature#L429)
- [apiOcm/searchFederationUsers.feature:601](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/searchFederationUsers.feature#L601)
#### [OCM. federated connection is not dropped when one of the users deletes the connection](https://github.com/owncloud/ocis/issues/10216)
- [apiOcm/deleteFederatedConnections.feature:21](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/deleteFederatedConnections.feature#L21)
- [apiOcm/deleteFederatedConnections.feature:67](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/deleteFederatedConnections.feature#L67)
#### [OCM. server crash after deleting share for ocm user](https://github.com/owncloud/ocis/issues/10213)
- [apiOcm/deleteFederatedConnections.feature:102](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/deleteFederatedConnections.feature#L102)
#### [Shares Jail PROPFIND returns different File IDs for the same item](https://github.com/owncloud/ocis/issues/9933)
- [apiSharingNg1/propfindShares.feature:149](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNg1/propfindShares.feature#L149)
#### [Readiness check for some services returns 500 status code](https://github.com/owncloud/ocis/issues/10661)
- [apiServiceAvailability/serviceAvailabilityCheck.feature:123](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L123)
#### [Skip tests for different languages](https://github.com/qsfera/server/issues/183)
- [apiActivities/activities.feature:2598](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiActivities/activities.feature#L2598)
#### [Missing properties in REPORT response](https://github.com/owncloud/ocis/issues/9780), [d:getetag property has empty value in REPORT response](https://github.com/owncloud/ocis/issues/9783)
- [apiSearch1/search.feature:437](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L437)
- [apiSearch1/search.feature:438](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L438)
- [apiSearch1/search.feature:439](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L439)
- [apiSearch1/search.feature:465](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L465)
- [apiSearch1/search.feature:466](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L466)
- [apiSearch1/search.feature:467](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L467)
## Scenarios from core API tests that are expected to fail with decomposed storage
### File
Basic file management like up and download, move, copy, properties, trash, versions and chunking.
#### [Custom dav properties with namespaces are rendered incorrectly](https://github.com/owncloud/ocis/issues/2140)
_ocdav: double-check the webdav property parsing when custom namespaces are used_
- [coreApiWebdavProperties/setFileProperties.feature:128](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/setFileProperties.feature#L128)
- [coreApiWebdavProperties/setFileProperties.feature:129](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/setFileProperties.feature#L129)
- [coreApiWebdavProperties/setFileProperties.feature:130](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/setFileProperties.feature#L130)
### Sync
Synchronization features like etag propagation, setting mtime and locking files
#### [Uploading an old method chunked file with checksum should fail using new DAV path](https://github.com/owncloud/ocis/issues/2323)
- [coreApiMain/checksums.feature:233](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiMain/checksums.feature#L233)
- [coreApiMain/checksums.feature:234](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiMain/checksums.feature#L234)
- [coreApiMain/checksums.feature:235](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiMain/checksums.feature#L235)
### Share
#### [d:quota-available-bytes in dprop of PROPFIND give wrong response value](https://github.com/owncloud/ocis/issues/8197)
- [coreApiWebdavProperties/getQuota.feature:57](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L57)
- [coreApiWebdavProperties/getQuota.feature:58](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L58)
- [coreApiWebdavProperties/getQuota.feature:59](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L59)
- [coreApiWebdavProperties/getQuota.feature:73](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L73)
- [coreApiWebdavProperties/getQuota.feature:74](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L74)
- [coreApiWebdavProperties/getQuota.feature:75](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L75)
#### [deleting a file inside a received shared folder is moved to the trash-bin of the sharer not the receiver](https://github.com/owncloud/ocis/issues/1124)
- [coreApiTrashbin/trashbinSharingToShares.feature:54](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L54)
- [coreApiTrashbin/trashbinSharingToShares.feature:55](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L55)
- [coreApiTrashbin/trashbinSharingToShares.feature:56](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L56)
- [coreApiTrashbin/trashbinSharingToShares.feature:83](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L83)
- [coreApiTrashbin/trashbinSharingToShares.feature:84](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L84)
- [coreApiTrashbin/trashbinSharingToShares.feature:85](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L85)
- [coreApiTrashbin/trashbinSharingToShares.feature:142](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L142)
- [coreApiTrashbin/trashbinSharingToShares.feature:143](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L143)
- [coreApiTrashbin/trashbinSharingToShares.feature:144](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L144)
- [coreApiTrashbin/trashbinSharingToShares.feature:202](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L202)
- [coreApiTrashbin/trashbinSharingToShares.feature:203](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L203)
### Other
API, search, favorites, config, capabilities, not existing endpoints, CORS and others
#### [sending MKCOL requests to another or non-existing user's webDav endpoints as normal user should return 404](https://github.com/owncloud/ocis/issues/5049)
_ocdav: api compatibility, return correct status code_
- [coreApiAuth/webDavMKCOLAuth.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavMKCOLAuth.feature#L42)
- [coreApiAuth/webDavMKCOLAuth.feature:53](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavMKCOLAuth.feature#L53)
#### [trying to lock file of another user gives http 500](https://github.com/owncloud/ocis/issues/2176)
- [coreApiAuth/webDavLOCKAuth.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavLOCKAuth.feature#L46)
- [coreApiAuth/webDavLOCKAuth.feature:58](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavLOCKAuth.feature#L58)
#### [WWW-Authenticate header for unauthenticated requests is not clear](https://github.com/owncloud/ocis/issues/2285)
- [coreApiWebdavOperations/refuseAccess.feature:21](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/refuseAccess.feature#L21)
- [coreApiWebdavOperations/refuseAccess.feature:22](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/refuseAccess.feature#L22)
#### [PATCH request for TUS upload with wrong checksum gives incorrect response](https://github.com/owncloud/ocis/issues/1755)
- [coreApiWebdavUploadTUS/checksums.feature:74](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L74)
- [coreApiWebdavUploadTUS/checksums.feature:75](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L75)
- [coreApiWebdavUploadTUS/checksums.feature:76](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L76)
- [coreApiWebdavUploadTUS/checksums.feature:77](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L77)
- [coreApiWebdavUploadTUS/checksums.feature:79](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L79)
- [coreApiWebdavUploadTUS/checksums.feature:78](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L78)
- [coreApiWebdavUploadTUS/checksums.feature:147](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L147)
- [coreApiWebdavUploadTUS/checksums.feature:148](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L148)
- [coreApiWebdavUploadTUS/checksums.feature:149](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L149)
- [coreApiWebdavUploadTUS/checksums.feature:192](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L192)
- [coreApiWebdavUploadTUS/checksums.feature:193](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L193)
- [coreApiWebdavUploadTUS/checksums.feature:194](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L194)
- [coreApiWebdavUploadTUS/checksums.feature:195](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L195)
- [coreApiWebdavUploadTUS/checksums.feature:196](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L196)
- [coreApiWebdavUploadTUS/checksums.feature:197](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L197)
- [coreApiWebdavUploadTUS/checksums.feature:240](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L240)
- [coreApiWebdavUploadTUS/checksums.feature:241](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L241)
- [coreApiWebdavUploadTUS/checksums.feature:242](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L242)
- [coreApiWebdavUploadTUS/checksums.feature:243](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L243)
- [coreApiWebdavUploadTUS/checksums.feature:244](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L244)
- [coreApiWebdavUploadTUS/checksums.feature:245](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L245)
- [coreApiWebdavUploadTUS/uploadToShare.feature:255](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L255)
- [coreApiWebdavUploadTUS/uploadToShare.feature:256](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L256)
- [coreApiWebdavUploadTUS/uploadToShare.feature:279](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L279)
- [coreApiWebdavUploadTUS/uploadToShare.feature:280](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L280)
- [coreApiWebdavUploadTUS/uploadToShare.feature:376](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L376)
- [coreApiWebdavUploadTUS/uploadToShare.feature:377](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L377)
#### [Trying to delete other user's trashbin item returns 409 for spaces path instead of 404](https://github.com/owncloud/ocis/issues/9791)
- [coreApiTrashbin/trashbinDelete.feature:92](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinDelete.feature#L92)
#### [MOVE a file into same folder with same name returns 404 instead of 403](https://github.com/owncloud/ocis/issues/1976)
- [coreApiWebdavMove/moveFile.feature:100](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFile.feature#L100)
- [coreApiWebdavMove/moveFile.feature:101](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFile.feature#L101)
- [coreApiWebdavMove/moveFile.feature:102](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFile.feature#L102)
- [coreApiWebdavMove/moveFolder.feature:217](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFolder.feature#L217)
- [coreApiWebdavMove/moveFolder.feature:218](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFolder.feature#L218)
- [coreApiWebdavMove/moveFolder.feature:219](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFolder.feature#L219)
- [coreApiWebdavMove/moveShareOnQsfera.feature:334](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveShareOnQsfera.feature#L334)
- [coreApiWebdavMove/moveShareOnQsfera.feature:337](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveShareOnQsfera.feature#L337)
- [coreApiWebdavMove/moveShareOnQsfera.feature:340](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveShareOnQsfera.feature#L340)
#### [COPY file/folder to same name is possible (but 500 code error for folder with spaces path)](https://github.com/owncloud/ocis/issues/8711)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:198](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L198)
- [coreApiWebdavCopyCreate/copyFile.feature:1094](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/copyFile.feature#L1094)
- [coreApiWebdavCopyCreate/copyFile.feature:1095](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/copyFile.feature#L1095)
- [coreApiWebdavCopyCreate/copyFile.feature:1096](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/copyFile.feature#L1096)
#### [Trying to restore personal file to file of share received folder returns 403 but the share file is deleted (new dav path)](https://github.com/owncloud/ocis/issues/10356)
- [coreApiTrashbin/trashbinSharingToShares.feature:277](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L277)
#### [Preview. UTF characters do not display on prievew](https://github.com/qsfera/server/issues/1451)
- [coreApiWebdavPreviews/previews.feature:249](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L249)
- [coreApiWebdavPreviews/previews.feature:250](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L250)
- [coreApiWebdavPreviews/previews.feature:251](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L251)
#### [Preview of text file truncated](https://github.com/qsfera/server/issues/1452)
- [coreApiWebdavPreviews/previews.feature:263](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L263)
- [coreApiWebdavPreviews/previews.feature:264](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L264)
- [coreApiWebdavPreviews/previews.feature:265](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L265)
#### [500 when user tries to delete group from space](https://github.com/qsfera/server/issues/2799)
- [apiSharingNg1/removeAccessToDrive.feature:145](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNg1/removeAccessToDrive.feature#L145)
### Won't fix
Not everything needs to be implemented for qsfera.
- _Blacklisted ignored files are no longer required because qsfera can handle `.htaccess` files without security implications introduced by serving user provided files with apache._
Note: always have an empty line at the end of this file.
The bash script that processes this file requires that the last line has a newline on the end.
@@ -0,0 +1,360 @@
## Scenarios from КуСфера API tests that are expected to fail with posix storage
#### [Downloading the archive of the resource (files | folder) using resource path is not possible](https://github.com/owncloud/ocis/issues/4637)
- [apiArchiver/downloadByPath.feature:25](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L25)
- [apiArchiver/downloadByPath.feature:26](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L26)
- [apiArchiver/downloadByPath.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L43)
- [apiArchiver/downloadByPath.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L44)
- [apiArchiver/downloadByPath.feature:47](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L47)
- [apiArchiver/downloadByPath.feature:73](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L73)
- [apiArchiver/downloadByPath.feature:171](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L171)
- [apiArchiver/downloadByPath.feature:172](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiArchiver/downloadByPath.feature#L172)
#### [PATCH request for TUS upload with wrong checksum gives incorrect response](https://github.com/owncloud/ocis/issues/1755)
- [apiSpacesShares/shareUploadTUS.feature:283](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L283)
- [apiSpacesShares/shareUploadTUS.feature:303](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L303)
- [apiSpacesShares/shareUploadTUS.feature:384](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareUploadTUS.feature#L384)
#### [Settings service user can list other peoples assignments](https://github.com/owncloud/ocis/issues/5032)
- [apiGraph/getAssignedRole.feature:31](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L31)
- [apiGraph/getAssignedRole.feature:32](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L32)
- [apiGraph/getAssignedRole.feature:33](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraph/getAssignedRole.feature#L33)
#### [A User can get information of another user with Graph API](https://github.com/owncloud/ocis/issues/5125)
- [apiGraphUserGroup/getUser.feature:84](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L84)
- [apiGraphUserGroup/getUser.feature:85](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L85)
- [apiGraphUserGroup/getUser.feature:86](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L86)
- [apiGraphUserGroup/getUser.feature:628](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L628)
- [apiGraphUserGroup/getUser.feature:629](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L629)
- [apiGraphUserGroup/getUser.feature:630](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L630)
- [apiGraphUserGroup/getUser.feature:645](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L645)
- [apiGraphUserGroup/getUser.feature:646](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L646)
- [apiGraphUserGroup/getUser.feature:647](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getUser.feature#L647)
#### [Normal user can get expanded members information of a group](https://github.com/owncloud/ocis/issues/5604)
- [apiGraphUserGroup/getGroup.feature:399](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L399)
- [apiGraphUserGroup/getGroup.feature:400](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L400)
- [apiGraphUserGroup/getGroup.feature:401](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L401)
- [apiGraphUserGroup/getGroup.feature:460](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L460)
- [apiGraphUserGroup/getGroup.feature:461](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L461)
- [apiGraphUserGroup/getGroup.feature:462](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L462)
- [apiGraphUserGroup/getGroup.feature:508](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L508)
- [apiGraphUserGroup/getGroup.feature:509](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L509)
- [apiGraphUserGroup/getGroup.feature:510](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/getGroup.feature#L510)
#### [Same users can be added in a group multiple time](https://github.com/owncloud/ocis/issues/5702)
- [apiGraphUserGroup/addUserToGroup.feature:295](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L295)
#### [Users are added in a group with wrong host in host-part of user](https://github.com/owncloud/ocis/issues/5871)
- [apiGraphUserGroup/addUserToGroup.feature:379](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L379)
- [apiGraphUserGroup/addUserToGroup.feature:393](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L393)
#### [Adding the same user as multiple members in a single request results in listing the same user twice in the group](https://github.com/owncloud/ocis/issues/5855)
- [apiGraphUserGroup/addUserToGroup.feature:430](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiGraphUserGroup/addUserToGroup.feature#L430)
#### [Shared file locking is not possible using different path](https://github.com/owncloud/ocis/issues/7599)
- [apiLocks/lockFiles.feature:185](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L185)
- [apiLocks/lockFiles.feature:186](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L186)
- [apiLocks/lockFiles.feature:187](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L187)
- [apiLocks/lockFiles.feature:309](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L309)
- [apiLocks/lockFiles.feature:310](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L310)
- [apiLocks/lockFiles.feature:311](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L311)
- [apiLocks/lockFiles.feature:364](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L364)
- [apiLocks/lockFiles.feature:365](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L365)
- [apiLocks/lockFiles.feature:366](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L366)
- [apiLocks/lockFiles.feature:367](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L367)
- [apiLocks/lockFiles.feature:368](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L368)
- [apiLocks/lockFiles.feature:369](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L369)
- [apiLocks/lockFiles.feature:399](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L399)
- [apiLocks/lockFiles.feature:400](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L400)
- [apiLocks/lockFiles.feature:401](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L401)
- [apiLocks/lockFiles.feature:402](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L402)
- [apiLocks/lockFiles.feature:403](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L403)
- [apiLocks/lockFiles.feature:404](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L404)
- [apiLocks/unlockFiles.feature:62](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L62)
- [apiLocks/unlockFiles.feature:63](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L63)
- [apiLocks/unlockFiles.feature:64](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L64)
- [apiLocks/unlockFiles.feature:171](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L171)
- [apiLocks/unlockFiles.feature:172](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L172)
- [apiLocks/unlockFiles.feature:173](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L173)
- [apiLocks/unlockFiles.feature:174](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L174)
- [apiLocks/unlockFiles.feature:175](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L175)
- [apiLocks/unlockFiles.feature:176](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L176)
- [apiLocks/unlockFiles.feature:199](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L199)
- [apiLocks/unlockFiles.feature:200](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L200)
- [apiLocks/unlockFiles.feature:201](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L201)
- [apiLocks/unlockFiles.feature:202](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L202)
- [apiLocks/unlockFiles.feature:203](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L203)
- [apiLocks/unlockFiles.feature:204](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L204)
- [apiLocks/unlockFiles.feature:227](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L227)
- [apiLocks/unlockFiles.feature:228](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L228)
- [apiLocks/unlockFiles.feature:229](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L229)
- [apiLocks/unlockFiles.feature:230](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L230)
- [apiLocks/unlockFiles.feature:231](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L231)
- [apiLocks/unlockFiles.feature:232](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L232)
#### [Folders can be locked and locking works partially](https://github.com/owncloud/ocis/issues/7641)
- [apiLocks/lockFiles.feature:443](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L443)
- [apiLocks/lockFiles.feature:444](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L444)
- [apiLocks/lockFiles.feature:445](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L445)
- [apiLocks/lockFiles.feature:446](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L446)
- [apiLocks/lockFiles.feature:447](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L447)
- [apiLocks/lockFiles.feature:448](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L448)
- [apiLocks/lockFiles.feature:417](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L417)
- [apiLocks/lockFiles.feature:418](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L418)
- [apiLocks/lockFiles.feature:419](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L419)
- [apiLocks/lockFiles.feature:420](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L420)
- [apiLocks/lockFiles.feature:421](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L421)
- [apiLocks/lockFiles.feature:422](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L422)
#### [Anonymous users can unlock a file shared to them through a public link if they get the lock token](https://github.com/owncloud/ocis/issues/7761)
- [apiLocks/unlockFiles.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L42)
- [apiLocks/unlockFiles.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L43)
- [apiLocks/unlockFiles.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L44)
- [apiLocks/unlockFiles.feature:45](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L45)
- [apiLocks/unlockFiles.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L46)
- [apiLocks/unlockFiles.feature:47](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L47)
#### [Trying to unlock a shared file with sharer's lock token gives 500](https://github.com/owncloud/ocis/issues/7767)
- [apiLocks/unlockFiles.feature:115](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L115)
- [apiLocks/unlockFiles.feature:116](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L116)
- [apiLocks/unlockFiles.feature:117](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L117)
- [apiLocks/unlockFiles.feature:118](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L118)
- [apiLocks/unlockFiles.feature:119](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L119)
- [apiLocks/unlockFiles.feature:120](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L120)
- [apiLocks/unlockFiles.feature:143](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L143)
- [apiLocks/unlockFiles.feature:144](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L144)
- [apiLocks/unlockFiles.feature:145](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L145)
- [apiLocks/unlockFiles.feature:146](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L146)
- [apiLocks/unlockFiles.feature:147](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L147)
- [apiLocks/unlockFiles.feature:148](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L148)
#### [Anonymous user trying lock a file shared to them through a public link gives 405](https://github.com/owncloud/ocis/issues/7790)
- [apiLocks/lockFiles.feature:532](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L532)
- [apiLocks/lockFiles.feature:533](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L533)
- [apiLocks/lockFiles.feature:534](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L534)
- [apiLocks/lockFiles.feature:535](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L535)
- [apiLocks/lockFiles.feature:554](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L554)
- [apiLocks/lockFiles.feature:555](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L555)
- [apiLocks/lockFiles.feature:556](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L556)
- [apiLocks/lockFiles.feature:557](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L557)
#### [sharee (editor role) MOVE a file by file-id into shared sub-folder returns 502](https://github.com/owncloud/ocis/issues/7617)
- [apiSpacesDavOperation/moveByFileId.feature:368](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L368)
- [apiSpacesDavOperation/moveByFileId.feature:591](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L591)
#### [MOVE a file into same folder with same name returns 404 instead of 403](https://github.com/owncloud/ocis/issues/1976)
- [apiSpacesShares/moveSpaces.feature:69](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L69)
- [apiSpacesShares/moveSpaces.feature:70](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L70)
- [apiSpacesShares/moveSpaces.feature:416](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/moveSpaces.feature#L416)
- [apiSpacesDavOperation/moveByFileId.feature:61](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L61)
- [apiSpacesDavOperation/moveByFileId.feature:174](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L174)
- [apiSpacesDavOperation/moveByFileId.feature:175](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L175)
- [apiSpacesDavOperation/moveByFileId.feature:176](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L176)
- [apiSpacesDavOperation/moveByFileId.feature:393](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesDavOperation/moveByFileId.feature#L393)
#### [OCM. admin cannot get federated users if he hasn't connection with them ](https://github.com/owncloud/ocis/issues/9829)
- [apiOcm/searchFederationUsers.feature:429](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/searchFederationUsers.feature#L429)
- [apiOcm/searchFederationUsers.feature:601](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/searchFederationUsers.feature#L601)
#### [OCM. federated connection is not dropped when one of the users deletes the connection](https://github.com/owncloud/ocis/issues/10216)
- [apiOcm/deleteFederatedConnections.feature:21](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/deleteFederatedConnections.feature#L21)
- [apiOcm/deleteFederatedConnections.feature:67](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/deleteFederatedConnections.feature#L67)
#### [OCM. server crash after deleting share for ocm user](https://github.com/owncloud/ocis/issues/10213)
- [apiOcm/deleteFederatedConnections.feature:102](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiOcm/deleteFederatedConnections.feature#L102)
#### [Shares Jail PROPFIND returns different File IDs for the same item](https://github.com/owncloud/ocis/issues/9933)
- [apiSharingNg1/propfindShares.feature:149](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNg1/propfindShares.feature#L149)
#### [Readiness check for some services returns 500 status code](https://github.com/owncloud/ocis/issues/10661)
- [apiServiceAvailability/serviceAvailabilityCheck.feature:123](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L123)
#### [Skip tests for different languages](https://github.com/qsfera/server/issues/183)
- [apiActivities/activities.feature:2598](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiActivities/activities.feature#L2598)
#### [Missing properties in REPORT response](https://github.com/owncloud/ocis/issues/9780), [d:getetag property has empty value in REPORT response](https://github.com/owncloud/ocis/issues/9783)
- [apiSearch1/search.feature:437](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L437)
- [apiSearch1/search.feature:438](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L438)
- [apiSearch1/search.feature:439](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L439)
- [apiSearch1/search.feature:465](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L465)
- [apiSearch1/search.feature:466](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L466)
- [apiSearch1/search.feature:467](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L467)
## Scenarios from core API tests that are expected to fail with posix storage
### File
Basic file management like up and download, move, copy, properties, trash, versions and chunking.
#### [Custom dav properties with namespaces are rendered incorrectly](https://github.com/owncloud/ocis/issues/2140)
_ocdav: double-check the webdav property parsing when custom namespaces are used_
- [coreApiWebdavProperties/setFileProperties.feature:128](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/setFileProperties.feature#L128)
- [coreApiWebdavProperties/setFileProperties.feature:129](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/setFileProperties.feature#L129)
- [coreApiWebdavProperties/setFileProperties.feature:130](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/setFileProperties.feature#L130)
### Sync
Synchronization features like etag propagation, setting mtime and locking files
#### [Uploading an old method chunked file with checksum should fail using new DAV path](https://github.com/owncloud/ocis/issues/2323)
- [coreApiMain/checksums.feature:233](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiMain/checksums.feature#L233)
- [coreApiMain/checksums.feature:234](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiMain/checksums.feature#L234)
- [coreApiMain/checksums.feature:235](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiMain/checksums.feature#L235)
### Share
#### [d:quota-available-bytes in dprop of PROPFIND give wrong response value](https://github.com/owncloud/ocis/issues/8197)
- [coreApiWebdavProperties/getQuota.feature:57](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L57)
- [coreApiWebdavProperties/getQuota.feature:58](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L58)
- [coreApiWebdavProperties/getQuota.feature:59](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L59)
- [coreApiWebdavProperties/getQuota.feature:73](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L73)
- [coreApiWebdavProperties/getQuota.feature:74](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L74)
- [coreApiWebdavProperties/getQuota.feature:75](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavProperties/getQuota.feature#L75)
#### [deleting a file inside a received shared folder is moved to the trash-bin of the sharer not the receiver](https://github.com/owncloud/ocis/issues/1124)
- [coreApiTrashbin/trashbinSharingToShares.feature:54](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L54)
- [coreApiTrashbin/trashbinSharingToShares.feature:55](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L55)
- [coreApiTrashbin/trashbinSharingToShares.feature:56](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L56)
- [coreApiTrashbin/trashbinSharingToShares.feature:83](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L83)
- [coreApiTrashbin/trashbinSharingToShares.feature:84](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L84)
- [coreApiTrashbin/trashbinSharingToShares.feature:85](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L85)
- [coreApiTrashbin/trashbinSharingToShares.feature:142](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L142)
- [coreApiTrashbin/trashbinSharingToShares.feature:143](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L143)
- [coreApiTrashbin/trashbinSharingToShares.feature:144](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L144)
- [coreApiTrashbin/trashbinSharingToShares.feature:202](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L202)
- [coreApiTrashbin/trashbinSharingToShares.feature:203](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L203)
### Other
API, search, favorites, config, capabilities, not existing endpoints, CORS and others
#### [sending MKCOL requests to another or non-existing user's webDav endpoints as normal user should return 404](https://github.com/owncloud/ocis/issues/5049)
_ocdav: api compatibility, return correct status code_
- [coreApiAuth/webDavMKCOLAuth.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavMKCOLAuth.feature#L42)
- [coreApiAuth/webDavMKCOLAuth.feature:53](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavMKCOLAuth.feature#L53)
#### [trying to lock file of another user gives http 500](https://github.com/owncloud/ocis/issues/2176)
- [coreApiAuth/webDavLOCKAuth.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavLOCKAuth.feature#L46)
- [coreApiAuth/webDavLOCKAuth.feature:58](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavLOCKAuth.feature#L58)
#### [WWW-Authenticate header for unauthenticated requests is not clear](https://github.com/owncloud/ocis/issues/2285)
- [coreApiWebdavOperations/refuseAccess.feature:21](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/refuseAccess.feature#L21)
- [coreApiWebdavOperations/refuseAccess.feature:22](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/refuseAccess.feature#L22)
#### [PATCH request for TUS upload with wrong checksum gives incorrect response](https://github.com/owncloud/ocis/issues/1755)
- [coreApiWebdavUploadTUS/checksums.feature:74](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L74)
- [coreApiWebdavUploadTUS/checksums.feature:75](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L75)
- [coreApiWebdavUploadTUS/checksums.feature:76](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L76)
- [coreApiWebdavUploadTUS/checksums.feature:77](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L77)
- [coreApiWebdavUploadTUS/checksums.feature:79](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L79)
- [coreApiWebdavUploadTUS/checksums.feature:78](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L78)
- [coreApiWebdavUploadTUS/checksums.feature:147](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L147)
- [coreApiWebdavUploadTUS/checksums.feature:148](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L148)
- [coreApiWebdavUploadTUS/checksums.feature:149](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L149)
- [coreApiWebdavUploadTUS/checksums.feature:192](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L192)
- [coreApiWebdavUploadTUS/checksums.feature:193](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L193)
- [coreApiWebdavUploadTUS/checksums.feature:194](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L194)
- [coreApiWebdavUploadTUS/checksums.feature:195](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L195)
- [coreApiWebdavUploadTUS/checksums.feature:196](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L196)
- [coreApiWebdavUploadTUS/checksums.feature:197](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L197)
- [coreApiWebdavUploadTUS/checksums.feature:240](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L240)
- [coreApiWebdavUploadTUS/checksums.feature:241](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L241)
- [coreApiWebdavUploadTUS/checksums.feature:242](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L242)
- [coreApiWebdavUploadTUS/checksums.feature:243](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L243)
- [coreApiWebdavUploadTUS/checksums.feature:244](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L244)
- [coreApiWebdavUploadTUS/checksums.feature:245](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L245)
- [coreApiWebdavUploadTUS/uploadToShare.feature:255](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L255)
- [coreApiWebdavUploadTUS/uploadToShare.feature:256](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L256)
- [coreApiWebdavUploadTUS/uploadToShare.feature:279](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L279)
- [coreApiWebdavUploadTUS/uploadToShare.feature:280](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L280)
- [coreApiWebdavUploadTUS/uploadToShare.feature:376](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L376)
- [coreApiWebdavUploadTUS/uploadToShare.feature:377](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L377)
#### [Trying to delete other user's trashbin item returns 409 for spaces path instead of 404](https://github.com/owncloud/ocis/issues/9791)
- [coreApiTrashbin/trashbinDelete.feature:92](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinDelete.feature#L92)
#### [MOVE a file into same folder with same name returns 404 instead of 403](https://github.com/owncloud/ocis/issues/1976)
- [coreApiWebdavMove/moveFile.feature:100](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFile.feature#L100)
- [coreApiWebdavMove/moveFile.feature:101](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFile.feature#L101)
- [coreApiWebdavMove/moveFile.feature:102](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFile.feature#L102)
- [coreApiWebdavMove/moveFolder.feature:217](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFolder.feature#L217)
- [coreApiWebdavMove/moveFolder.feature:218](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFolder.feature#L218)
- [coreApiWebdavMove/moveFolder.feature:219](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveFolder.feature#L219)
- [coreApiWebdavMove/moveShareOnQsfera.feature:334](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveShareOnQsfera.feature#L334)
- [coreApiWebdavMove/moveShareOnQsfera.feature:337](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveShareOnQsfera.feature#L337)
- [coreApiWebdavMove/moveShareOnQsfera.feature:340](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavMove/moveShareOnQsfera.feature#L340)
#### [COPY file/folder to same name is possible (but 500 code error for folder with spaces path)](https://github.com/owncloud/ocis/issues/8711)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:198](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L198)
- [coreApiWebdavCopyCreate/copyFile.feature:1094](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/copyFile.feature#L1094)
- [coreApiWebdavCopyCreate/copyFile.feature:1095](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/copyFile.feature#L1095)
- [coreApiWebdavCopyCreate/copyFile.feature:1096](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/copyFile.feature#L1096)
#### [Trying to restore personal file to file of share received folder returns 403 but the share file is deleted (new dav path)](https://github.com/owncloud/ocis/issues/10356)
- [coreApiTrashbin/trashbinSharingToShares.feature:277](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L277)
#### [Preview. UTF characters do not display on prievew](https://github.com/qsfera/server/issues/1451)
- [coreApiWebdavPreviews/previews.feature:249](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L249)
- [coreApiWebdavPreviews/previews.feature:250](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L250)
- [coreApiWebdavPreviews/previews.feature:251](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L251)
#### [Preview of text file truncated](https://github.com/qsfera/server/issues/1452)
- [coreApiWebdavPreviews/previews.feature:263](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L263)
- [coreApiWebdavPreviews/previews.feature:264](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L264)
- [coreApiWebdavPreviews/previews.feature:265](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L265)
### TODO: adjust test
- [apiSharingNg1/removeAccessToDrive.feature:145](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNg1/removeAccessToDrive.feature#L145)
### Won't fix
Not everything needs to be implemented for qsfera.
- _Blacklisted ignored files are no longer required because qsfera can handle `.htaccess` files without security implications introduced by serving user provided files with apache._
Note: always have an empty line at the end of this file.
The bash script that processes this file requires that the last line has a newline on the end.
@@ -0,0 +1,366 @@
## Scenarios that are expected to fail when remote.php is not used
#### [REPORT request without remote.php returns empty result (only with dav/spaces path)](https://github.com/owncloud/ocis/issues/10329)
- [apiContract/sharesReport.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L43)
- [apiContract/sharesReport.feature:84](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L84)
- [apiContract/sharesReport.feature:150](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L150)
- [apiContract/sharesReport.feature:180](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L180)
- [apiContract/spacesReport.feature:16](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L16)
- [apiContract/spacesReport.feature:35](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L35)
- [apiContract/spacesReport.feature:55](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L55)
- [apiContract/spacesReport.feature:74](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L74)
- [apiContract/spacesSharesReport.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/spacesSharesReport.feature#L46)
- [apiContract/spacesSharesReport.feature:77](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiContract/spacesSharesReport.feature#L77)
- [apiSearch1/dateSearch.feature:19](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L19)
- [apiSearch1/dateSearch.feature:39](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L39)
- [apiSearch1/dateSearch.feature:40](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L40)
- [apiSearch1/dateSearch.feature:41](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L41)
- [apiSearch1/dateSearch.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L42)
- [apiSearch1/dateSearch.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L43)
- [apiSearch1/dateSearch.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L44)
- [apiSearch1/dateSearch.feature:45](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L45)
- [apiSearch1/dateSearch.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L46)
- [apiSearch1/dateSearch.feature:47](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L47)
- [apiSearch1/dateSearch.feature:48](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L48)
- [apiSearch1/dateSearch.feature:50](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L50)
- [apiSearch1/dateSearch.feature:53](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L53)
- [apiSearch1/search.feature:113](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L113)
- [apiSearch1/search.feature:176](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L176)
- [apiSearch1/search.feature:280](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L280)
- [apiSearch1/search.feature:304](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L304)
- [apiSearch1/search.feature:317](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L317)
- [apiSearch1/search.feature:318](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L318)
- [apiSearch1/search.feature:319](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L319)
- [apiSearch1/search.feature:320](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L320)
- [apiSearch1/search.feature:321](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L321)
- [apiSearch1/search.feature:324](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L324)
- [apiSearch1/search.feature:356](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L356)
- [apiSearch1/search.feature:369](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L369)
- [apiSearch1/search.feature:396](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L396)
- [apiSearch1/search.feature:410](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch1/search.feature#L410)
- [apiSearch2/mediaTypeSearch.feature:31](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L31)
- [apiSearch2/mediaTypeSearch.feature:32](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L32)
- [apiSearch2/mediaTypeSearch.feature:33](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L33)
- [apiSearch2/mediaTypeSearch.feature:34](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L34)
- [apiSearch2/mediaTypeSearch.feature:35](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L35)
- [apiSearch2/mediaTypeSearch.feature:36](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L36)
- [apiSearch2/mediaTypeSearch.feature:37](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L37)
- [apiSearch2/mediaTypeSearch.feature:38](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L38)
- [apiSearch2/mediaTypeSearch.feature:39](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L39)
- [apiSearch2/mediaTypeSearch.feature:60](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L60)
- [apiSearch2/mediaTypeSearch.feature:61](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L61)
- [apiSearch2/mediaTypeSearch.feature:62](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L62)
- [apiSearch2/mediaTypeSearch.feature:63](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L63)
- [apiSearch2/mediaTypeSearch.feature:64](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L64)
- [apiSearch2/mediaTypeSearch.feature:65](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L65)
- [apiSearch2/mediaTypeSearch.feature:66](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L66)
- [apiSearch2/mediaTypeSearch.feature:67](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L67)
- [apiSearch2/mediaTypeSearch.feature:68](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L68)
- [apiSearch2/mediaTypeSearch.feature:90](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L90)
- [apiSearch2/mediaTypeSearch.feature:91](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L91)
- [apiSearch2/mediaTypeSearch.feature:92](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L92)
- [apiSearch2/mediaTypeSearch.feature:93](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L93)
- [apiSearch2/mediaTypeSearch.feature:94](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L94)
- [apiSearch2/mediaTypeSearch.feature:95](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L95)
- [apiSearch2/mediaTypeSearch.feature:96](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L96)
- [apiSearch2/mediaTypeSearch.feature:97](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L97)
- [apiSearch2/mediaTypeSearch.feature:98](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L98)
- [apiSearch2/mediaTypeSearch.feature:126](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L126)
- [apiSearch2/mediaTypeSearch.feature:127](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L127)
- [apiSearch2/mediaTypeSearch.feature:128](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L128)
- [apiSearch2/mediaTypeSearch.feature:129](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L129)
- [apiSearch2/mediaTypeSearch.feature:130](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L130)
- [apiSearch2/mediaTypeSearch.feature:131](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L131)
- [apiSearch2/mediaTypeSearch.feature:132](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L132)
- [apiSearch2/mediaTypeSearch.feature:133](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L133)
- [apiSearch2/mediaTypeSearch.feature:134](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L134)
- [apiSearch2/mediaTypeSearch.feature:161](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L161)
- [apiSearch2/mediaTypeSearch.feature:162](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L162)
- [apiSearch2/mediaTypeSearch.feature:163](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L163)
- [apiSearch2/mediaTypeSearch.feature:164](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L164)
- [apiSearch2/mediaTypeSearch.feature:165](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L165)
- [apiSearch2/mediaTypeSearch.feature:166](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L166)
- [apiSearch2/mediaTypeSearch.feature:167](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L167)
- [apiSearch2/mediaTypeSearch.feature:168](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L168)
- [apiSearch2/mediaTypeSearch.feature:169](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L169)
- [apiSearch2/mediaTypeSearch.feature:172](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/mediaTypeSearch.feature#L172)
- [apiSearch2/tagSearch.feature:36](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L36)
- [apiSearch2/tagSearch.feature:64](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L64)
- [apiSearch2/tagSearch.feature:85](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L85)
- [apiSearch2/tagSearch.feature:108](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L108)
- [apiSearch2/tagSearch.feature:138](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L138)
- [apiSearch2/tagSearch.feature:172](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L172)
- [apiSearch2/tagSearch.feature:207](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L207)
- [apiSearch2/tagSearch.feature:226](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L226)
- [apiSearch2/tagSearch.feature:254](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L254)
- [apiSearch2/tagSearch.feature:255](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L255)
- [apiSearch2/tagSearch.feature:256](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L256)
- [apiSearch2/tagSearch.feature:257](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L257)
- [apiSearch2/tagSearch.feature:258](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L258)
- [apiSearch2/tagSearch.feature:259](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L259)
- [apiSearch2/tagSearch.feature:260](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L260)
- [apiSearch2/tagSearch.feature:261](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L261)
- [apiSearch2/tagSearch.feature:262](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L262)
- [apiSearch2/tagSearch.feature:263](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L263)
- [apiSearch2/tagSearch.feature:265](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearch2/tagSearch.feature#L265)
- [apiSearchContent/contentSearch.feature:27](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L27)
- [apiSearchContent/contentSearch.feature:51](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L51)
- [apiSearchContent/contentSearch.feature:84](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L84)
- [apiSearchContent/contentSearch.feature:112](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L112)
- [apiSearchContent/contentSearch.feature:131](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L131)
- [apiSearchContent/contentSearch.feature:147](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L147)
- [apiSearchContent/contentSearch.feature:163](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L163)
- [apiSearchContent/contentSearch.feature:186](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L186)
- [apiSearchContent/contentSearch.feature:215](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L215)
- [apiSearchContent/contentSearch.feature:233](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L233)
- [apiSearchContent/contentSearch.feature:234](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L234)
- [apiSearchContent/contentSearch.feature:235](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L235)
- [apiSearchContent/contentSearch.feature:236](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L236)
- [apiSearchContent/contentSearch.feature:237](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L237)
- [apiSearchContent/contentSearch.feature:238](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L238)
- [apiSearchContent/contentSearch.feature:267](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSearchContent/contentSearch.feature#L267)
- [cliCommands/searchReIndex.feature:15](https://github.com/qsfera/server/blob/main/tests/acceptance/features/cliCommands/searchReIndex.feature#L15)
#### [Downloading public files without remote.php returns 401 unauthorized error](https://github.com/owncloud/ocis/issues/9724)
- [apiSpaces/editPublicLinkOfSpace.feature:48](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/editPublicLinkOfSpace.feature#L48)
- [apiSpaces/editPublicLinkOfSpace.feature:49](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/editPublicLinkOfSpace.feature#L49)
- [apiSpacesShares/publicLinkDownload.feature:16](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/publicLinkDownload.feature#L16)
- [apiSpacesShares/publicLinkDownload.feature:32](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/publicLinkDownload.feature#L32)
- [apiSpacesShares/shareSpacesViaLink.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareSpacesViaLink.feature#L42)
- [apiSpacesShares/shareSpacesViaLink.feature:43](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareSpacesViaLink.feature#L43)
- [apiSpacesShares/shareSpacesViaLink.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareSpacesViaLink.feature#L44)
- [apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature:146](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature#L146)
- [apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature:147](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature#L147)
- [apiSharingNgLinkSharePermission/createLinkShare.feature:473](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNgLinkSharePermission/createLinkShare.feature#L473)
- [apiSharingNgLinkSharePermission/createLinkShare.feature:1208](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNgLinkSharePermission/createLinkShare.feature#L1208)
- [apiSharingNgLinkSharePermission/updateLinkShare.feature:204](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNgLinkSharePermission/updateLinkShare.feature#L204)
- [apiSharingNgLinkSharePermission/updateLinkShare.feature:282](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNgLinkSharePermission/updateLinkShare.feature#L282)
- [apiSharingNgLinkShareRoot/updateLinkShare.feature:10](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNgLinkShareRoot/updateLinkShare.feature#L10)
- [apiSharingNgLinkShareRoot/updateLinkShare.feature:42](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSharingNgLinkShareRoot/updateLinkShare.feature#L42)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:220](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L220)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:221](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L221)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:59](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L59)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:60](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L60)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:88](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L88)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:89](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L89)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:122](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L122)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:123](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L123)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:209](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L209)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:210](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L210)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:229](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L229)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:230](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L230)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:257](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L257)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:258](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L258)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:273](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L273)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:274](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L274)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:277](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L277)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:291](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L291)
- [coreApiSharePublicLink2/updatePublicLinkShare.feature:111](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/updatePublicLinkShare.feature#L111)
- [coreApiSharePublicLink2/updatePublicLinkShare.feature:112](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/updatePublicLinkShare.feature#L112)
- [coreApiSharePublicLink2/updatePublicLinkShare.feature:249](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/updatePublicLinkShare.feature#L249)
- [coreApiSharePublicLink2/updatePublicLinkShare.feature:250](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/updatePublicLinkShare.feature#L250)
- [coreApiSharePublicLink2/updatePublicLinkShare.feature:276](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/updatePublicLinkShare.feature#L276)
- [coreApiSharePublicLink2/updatePublicLinkShare.feature:277](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/updatePublicLinkShare.feature#L277)
#### [Most (if not all) requests to public link share without remote.php returns 401 Unauthorized error](https://github.com/owncloud/ocis/issues/10331)
- [apiSpaces/publicLink.feature:18](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/publicLink.feature#L18)
- [apiSpaces/publicLink.feature:23](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/publicLink.feature#L23)
- [apiSpaces/publicLink.feature:28](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/publicLink.feature#L28)
- [apiSpaces/publicLink.feature:34](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/publicLink.feature#L34)
- [apiSpaces/publicLink.feature:40](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/publicLink.feature#L40)
- [apiSpaces/uploadSpaces.feature:95](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/uploadSpaces.feature#L95)
- [apiSpaces/uploadSpaces.feature:112](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/uploadSpaces.feature#L112)
- [apiSpaces/uploadSpaces.feature:129](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/uploadSpaces.feature#L129)
- [apiSpacesShares/shareSpacesViaLink.feature:58](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpacesShares/shareSpacesViaLink.feature#L58)
- [apiDepthInfinity/propfind.feature:74](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiDepthInfinity/propfind.feature#L74)
- [apiDepthInfinity/propfind.feature:124](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiDepthInfinity/propfind.feature#L124)
- [apiLocks/lockFiles.feature:490](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L490)
- [apiLocks/lockFiles.feature:487](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L487)
- [apiLocks/lockFiles.feature:488](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L488)
- [apiLocks/lockFiles.feature:489](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L489)
- [apiLocks/lockFiles.feature:513](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L513)
- [apiLocks/lockFiles.feature:510](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L510)
- [apiLocks/lockFiles.feature:511](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L511)
- [apiLocks/lockFiles.feature:512](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/lockFiles.feature#L512)
- [apiLocks/unlockFiles.feature:320](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L320)
- [apiLocks/unlockFiles.feature:321](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L321)
- [apiLocks/unlockFiles.feature:322](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L322)
- [apiLocks/unlockFiles.feature:323](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L323)
- [apiAntivirus/antivirus.feature:114](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L114)
- [apiAntivirus/antivirus.feature:115](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L115)
- [apiAntivirus/antivirus.feature:116](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L116)
- [apiAntivirus/antivirus.feature:117](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L117)
- [apiAntivirus/antivirus.feature:118](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L118)
- [apiAntivirus/antivirus.feature:119](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L119)
- [apiAntivirus/antivirus.feature:140](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L140)
- [apiAntivirus/antivirus.feature:141](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L141)
- [apiAntivirus/antivirus.feature:142](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L142)
- [apiAntivirus/antivirus.feature:143](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L143)
- [apiAntivirus/antivirus.feature:144](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L144)
- [apiAntivirus/antivirus.feature:145](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L145)
- [apiAntivirus/antivirus.feature:357](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L357)
- [apiAntivirus/antivirus.feature:358](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L358)
- [apiAntivirus/antivirus.feature:359](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L359)
- [apiCollaboration/wopi.feature:956](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L956)
- [apiCollaboration/wopi.feature:957](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L957)
- [apiCollaboration/wopi.feature:958](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L958)
- [apiCollaboration/wopi.feature:961](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L961)
- [apiCollaboration/wopi.feature:1047](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L1047)
- [apiCollaboration/wopi.feature:1048](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L1048)
- [apiCollaboration/wopi.feature:1049](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L1049)
- [apiCollaboration/wopi.feature:1052](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiCollaboration/wopi.feature#L1052)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:27](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L27)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:28](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L28)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:29](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L29)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:30](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L30)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:33](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L33)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L46)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:70](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L70)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:95](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L95)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:120](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L120)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:132](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L132)
- [coreApiSharePublicLink1/changingPublicLinkShare.feature:145](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/changingPublicLinkShare.feature#L145)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:189](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L189)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:190](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L190)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:304](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L304)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:328](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L328)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:329](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L329)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:344](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L344)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:345](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L345)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:348](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L348)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:363](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L363)
- [coreApiSharePublicLink1/createPublicLinkShare.feature:377](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L377)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:13](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L13)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:28](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L28)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L44)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:60](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L60)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:75](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L75)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:92](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L92)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:105](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L105)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:137](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L137)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:138](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L138)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:139](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L139)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:140](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L140)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:158](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L158)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:159](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L159)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:160](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L160)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:161](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L161)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:180](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L180)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:181](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L181)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:182](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L182)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:183](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L183)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:186](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L186)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:212](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L212)
- [coreApiSharePublicLink2/copyFromPublicLink.feature:225](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L225)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:28](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L28)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:29](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L29)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:30](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L30)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:33](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L33)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:44](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L44)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:58](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L58)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:70](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L70)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:82](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L82)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:94](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L94)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:106](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L106)
- [coreApiSharePublicLink2/uploadToPublicLinkShare.feature:118](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiSharePublicLink2/uploadToPublicLinkShare.feature#L118)
- [coreApiWebdavEtagPropagation1/deleteFileFolder.feature:238](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/deleteFileFolder.feature#L238)
- [coreApiWebdavEtagPropagation1/deleteFileFolder.feature:239](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/deleteFileFolder.feature#L239)
- [coreApiWebdavEtagPropagation1/deleteFileFolder.feature:240](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/deleteFileFolder.feature#L240)
- [coreApiWebdavEtagPropagation1/deleteFileFolder.feature:262](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/deleteFileFolder.feature#L262)
- [coreApiWebdavEtagPropagation1/deleteFileFolder.feature:263](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/deleteFileFolder.feature#L263)
- [coreApiWebdavEtagPropagation1/deleteFileFolder.feature:264](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/deleteFileFolder.feature#L264)
- [coreApiWebdavEtagPropagation1/moveFileFolder.feature:308](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/moveFileFolder.feature#L308)
- [coreApiWebdavEtagPropagation1/moveFileFolder.feature:309](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/moveFileFolder.feature#L309)
- [coreApiWebdavEtagPropagation1/moveFileFolder.feature:310](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/moveFileFolder.feature#L310)
- [coreApiWebdavEtagPropagation1/moveFileFolder.feature:333](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/moveFileFolder.feature#L333)
- [coreApiWebdavEtagPropagation1/moveFileFolder.feature:334](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/moveFileFolder.feature#L334)
- [coreApiWebdavEtagPropagation1/moveFileFolder.feature:335](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation1/moveFileFolder.feature#L335)
- [coreApiWebdavEtagPropagation2/copyFileFolder.feature:135](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/copyFileFolder.feature#L135)
- [coreApiWebdavEtagPropagation2/copyFileFolder.feature:136](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/copyFileFolder.feature#L136)
- [coreApiWebdavEtagPropagation2/copyFileFolder.feature:137](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/copyFileFolder.feature#L137)
- [coreApiWebdavEtagPropagation2/createFolder.feature:111](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/createFolder.feature#L111)
- [coreApiWebdavEtagPropagation2/upload.feature:188](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/upload.feature#L188)
- [coreApiWebdavEtagPropagation2/upload.feature:189](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/upload.feature#L189)
- [coreApiWebdavEtagPropagation2/upload.feature:190](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavEtagPropagation2/upload.feature#L190)
- [coreApiWebdavOperations/listFiles.feature:112](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L112)
- [coreApiWebdavOperations/listFiles.feature:140](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L140)
- [coreApiWebdavOperations/propfind.feature:40](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L40)
- [coreApiWebdavOperations/propfind.feature:55](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L55)
- [coreApiWebdavOperations/propfind.feature:69](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L69)
- [coreApiWebdavUpload/uploadFile.feature:376](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUpload/uploadFile.feature#L376)
- [apiActivities/shareActivities.feature:1956](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiActivities/shareActivities.feature#L1956)
- [apiActivities/shareActivities.feature:2095](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiActivities/shareActivities.feature#L2095)
#### [Cannot create new TUS upload resource using /webdav without remote.php - returns html instead](https://github.com/owncloud/ocis/issues/10346)
- [apiSpaces/tusUpload.feature:60](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/tusUpload.feature#L60)
- [apiSpaces/tusUpload.feature:104](https://github.com/qsfera/server/blob/main/tests/acceptance/features/apiSpaces/tusUpload.feature#L104)
- [coreApiWebdavUploadTUS/creationWithUploadExtension.feature:38](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/creationWithUploadExtension.feature#L38)
- [coreApiWebdavUploadTUS/uploadFile.feature:16](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L16)
- [coreApiWebdavUploadTUS/uploadFile.feature:17](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L17)
- [coreApiWebdavUploadTUS/uploadFile.feature:18](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L18)
- [coreApiWebdavUploadTUS/uploadFile.feature:19](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L19)
- [coreApiWebdavUploadTUS/uploadFile.feature:20](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L20)
- [coreApiWebdavUploadTUS/uploadFile.feature:21](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L21)
- [coreApiWebdavUploadTUS/uploadFile.feature:22](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L22)
- [coreApiWebdavUploadTUS/uploadFile.feature:46](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L46)
- [coreApiWebdavUploadTUS/uploadFile.feature:47](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L47)
- [coreApiWebdavUploadTUS/uploadFile.feature:48](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L48)
- [coreApiWebdavUploadTUS/uploadFile.feature:49](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L49)
- [coreApiWebdavUploadTUS/uploadFile.feature:50](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L50)
- [coreApiWebdavUploadTUS/uploadFile.feature:51](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L51)
- [coreApiWebdavUploadTUS/uploadFile.feature:52](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L52)
- [coreApiWebdavUploadTUS/uploadFile.feature:75](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L75)
- [coreApiWebdavUploadTUS/uploadFile.feature:86](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L86)
- [coreApiWebdavUploadTUS/uploadFile.feature:98](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L98)
- [coreApiWebdavUploadTUS/uploadFile.feature:109](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L109)
- [coreApiWebdavUploadTUS/uploadFile.feature:122](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L122)
- [coreApiWebdavUploadTUS/uploadFile.feature:133](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L133)
- [coreApiWebdavUploadTUS/uploadFile.feature:146](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L146)
- [coreApiWebdavUploadTUS/uploadFile.feature:187](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L187)
- [coreApiWebdavUploadTUS/uploadFile.feature:199](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L199)
- [coreApiWebdavUploadTUS/uploadFile.feature:212](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFile.feature#L212)
- [coreApiWebdavUploadTUS/uploadFileMtime.feature:16](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtime.feature#L16)
- [coreApiWebdavUploadTUS/uploadFileMtime.feature:27](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtime.feature#L27)
- [coreApiWebdavUploadTUS/uploadFileMtime.feature:39](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtime.feature#L39)
- [coreApiWebdavUploadTUS/uploadFileMtime.feature:51](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtime.feature#L51)
- [coreApiWebdavUploadTUS/uploadFileMtime.feature:65](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtime.feature#L65)
- [coreApiWebdavUploadTUS/uploadFileMtime.feature:79](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtime.feature#L79)
- [coreApiWebdavUploadTUS/uploadFileMtimeShares.feature:29](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtimeShares.feature#L29)
- [coreApiWebdavUploadTUS/uploadFileMtimeShares.feature:48](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtimeShares.feature#L48)
- [coreApiWebdavUploadTUS/uploadFileMtimeShares.feature:69](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtimeShares.feature#L69)
- [coreApiWebdavUploadTUS/uploadFileMtimeShares.feature:89](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadFileMtimeShares.feature#L89)
- [coreApiWebdavUploadTUS/uploadToMoveFolder.feature:20](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToMoveFolder.feature#L20)
- [coreApiWebdavUploadTUS/uploadToShare.feature:29](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L29)
- [coreApiWebdavUploadTUS/uploadToShare.feature:48](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L48)
- [coreApiWebdavUploadTUS/uploadToShare.feature:69](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L69)
- [coreApiWebdavUploadTUS/uploadToShare.feature:89](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L89)
#### [PROPFIND to /webdav root (old dav path) without remote.php returns html instead of xml](https://github.com/owncloud/ocis/issues/10334)
- [coreApiAuth/webDavAuth.feature:15](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavAuth.feature#L15)
- [coreApiAuth/webDavAuth.feature:25](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiAuth/webDavAuth.feature#L25)
- [coreApiShareManagementToShares/moveReceivedShare.feature:49](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiShareManagementToShares/moveReceivedShare.feature#L49)
- [coreApiShareManagementToShares/moveReceivedShare.feature:130](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiShareManagementToShares/moveReceivedShare.feature#L130)
- [coreApiShareManagementToShares/moveReceivedShare.feature:129](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiShareManagementToShares/moveReceivedShare.feature#L129)
- [coreApiWebdavOperations/propfind.feature:25](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L25)
- [coreApiWebdavOperations/propfind.feature:26](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L26)
- [coreApiWebdavOperations/propfind.feature:36](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L36)
#### [Public cannot download file preview of unprotected (without password) link share without remote.php](https://github.com/owncloud/ocis/issues/10341)
- [coreApiWebdavPreviews/linkSharePreviews.feature:25](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/linkSharePreviews.feature#L25)
- [coreApiWebdavPreviews/linkSharePreviews.feature:54](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavPreviews/linkSharePreviews.feature#L54)
#### [Trying to create .. resource with /webdav root (old dav path) without remote.php returns html](https://github.com/owncloud/ocis/issues/10339)
- [coreApiWebdavCopyCreate/createFileFolder.feature:176](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/createFileFolder.feature#L176)
- [coreApiWebdavCopyCreate/createFileFolder.feature:177](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/createFileFolder.feature#L177)
- [coreApiWebdavCopyCreate/createFileFolder.feature:196](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/createFileFolder.feature#L196)
- [coreApiWebdavCopyCreate/createFileFolder.feature:197](https://github.com/qsfera/server/blob/main/tests/acceptance/features/coreApiWebdavCopyCreate/createFileFolder.feature#L197)
Note: always have an empty line at the end of this file.
The bash script that processes this file requires that the last line has a newline on the end.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,484 @@
@antivirus @skipOnReva
Feature: antivirus
As a system administrator and user
I want to protect myself and others from known viruses
So that I can prevent files with viruses from being uploaded
Background:
Given user "Alice" has been created with default attributes
Scenario Outline: upload a normal file without virus
Given using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/textfile.txt" to "/normalfile.txt" using the WebDAV API
Then the HTTP status code should be "201"
And as "Alice" file "/normalfile.txt" should exist
And the content of file "/normalfile.txt" for user "Alice" should be:
"""
This is a testfile.
Cheers.
"""
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: upload a file with virus
Given using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/filesWithVirus/<file-name>" to "<new-file-name>" using the WebDAV API
# antivirus service can scan files during post-processing. on demand scanning is currently not available
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
And as "Alice" file "<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
Scenario Outline: upload a file with virus and a file without virus
Given using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/filesWithVirus/<file-name>" to "<new-file-name>" using the WebDAV API
# antivirus service can scan files during post-processing. on demand scanning is currently not available
Then the HTTP status code should be "201"
And user "Alice" uploads file "filesForUpload/textfile.txt" to "/normalfile.txt" using the WebDAV API
And the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
And as "Alice" file "<new-file-name>" should not exist
But as "Alice" file "/normalfile.txt" should exist
And the content of file "/normalfile.txt" for user "Alice" should be:
"""
This is a testfile.
Cheers.
"""
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
Scenario Outline: upload a file with virus in chunks
Given using <dav-path-version> DAV path
When user "Alice" uploads the following chunks to "/myChunkedFile.txt" with old chunking and using the WebDAV API
| number | content |
| 1 | X5O!P%@AP[4\PZX54(P^)7C |
| 2 | C)7}$EICAR-STANDARD-ANT |
| 3 | IVIRUS-TEST-FILE!$H+H* |
# antivirus service can scan files during post-processing. on demand scanning is currently not available
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| Virus found in myChunkedFile.txt. Upload not possible. Virus: |
And as "Alice" file "/myChunkedFile.txt" should not exist
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@issue-10331 @env-config
Scenario Outline: public uploads a file with the virus to a public share
Given using <dav-path-version> DAV path
And the config "OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD" has been set to "false"
And using SharingNG
And user "Alice" has created folder "/uploadFolder"
And user "Alice" has created the following resource link share:
| resource | uploadFolder |
| space | Personal |
| permissionsRole | edit |
| expirationDateTime | 2040-01-01T23:59:59.000Z |
When the public uploads file "filesForUpload/filesWithVirus/<file-name>" to "<new-file-name>" inside last link shared folder using the public WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
@issue-10331
Scenario Outline: public uploads a file with the virus to a password-protected public share
Given using <dav-path-version> DAV path
And using SharingNG
And user "Alice" has created folder "/uploadFolder"
And user "Alice" has created the following resource link share:
| resource | uploadFolder |
| space | Personal |
| permissionsRole | edit |
| password | %public% |
| expirationDateTime | 2040-01-01T23:59:59.000Z |
When the public uploads file "filesForUpload/filesWithVirus/<file-name>" to "<new-file-name>" inside last link shared folder with password "%public%" using the public WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
Scenario Outline: upload a file with virus to a user share
Given using <dav-path-version> DAV path
And user "Brian" has been created with default attributes
And user "Alice" has created folder "uploadFolder"
And user "Alice" has sent the following resource share invitation:
| resource | uploadFolder |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Editor |
And user "Brian" has a share "uploadFolder" synced
When user "Brian" uploads file "filesForUpload/filesWithVirus/<file-name>" to "/Shares/uploadFolder/<new-file-name>" using the WebDAV API
Then the HTTP status code should be "201"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| <message> |
And as "Brian" file "/Shares/uploadFolder/<new-file-name>" should not exist
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
Scenario Outline: upload a file with virus to a group share
Given using <dav-path-version> DAV path
And user "Brian" has been created with default attributes
And group "group1" has been created
And user "Brian" has been added to group "group1"
And user "Alice" has created folder "uploadFolder"
And user "Alice" has sent the following resource share invitation:
| resource | uploadFolder |
| space | Personal |
| sharee | group1 |
| shareType | group |
| permissionsRole | Editor |
And user "Brian" has a share "uploadFolder" synced
When user "Brian" uploads file "filesForUpload/filesWithVirus/<file-name>" to "/Shares/uploadFolder/<new-file-name>" using the WebDAV API
Then the HTTP status code should be "201"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| <message> |
When the user waits for "10" seconds for postprocessing to finish
And as "Brian" file "/Shares/uploadFolder/<new-file-name>" should not exist
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
Scenario Outline: upload a file with virus to a project space
Given using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has created a folder "uploadFolder" in space "new-space"
When user "Alice" uploads a file "filesForUpload/filesWithVirus/<file-name>" to "/uploadFolder/<new-file-name>" in space "new-space" using the WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification for resource "<new-file-name>" with subject "Virus found" and message:
| message |
| <message> |
And for user "Alice" folder "uploadFolder" of the space "new-space" should not contain these entries:
| <new-file-name> |
When user "Alice" uploads a file "filesForUpload/filesWithVirus/<file-name>" to "/<new-file-name>" in space "new-space" using the WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification for resource "<new-file-name>" with subject "Virus found" and message:
| message |
| <message> |
And for user "Alice" the space "new-space" should not contain these entries:
| /<new-file-name> |
Examples:
| file-name | new-file-name | message |
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
Scenario Outline: upload a file with virus to a shared project space
Given using spaces DAV path
And user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Editor |
When user "Brian" uploads a file "/filesForUpload/filesWithVirus/<file-name>" to "/<new-file-name>" in space "new-space" using the WebDAV API
Then the HTTP status code should be "201"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| <message> |
And for user "Brian" the space "new-space" should not contain these entries:
| /<new-file-name> |
And for user "Alice" the space "new-space" should not contain these entries:
| /<new-file-name> |
Examples:
| file-name | new-file-name | message |
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: |
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: |
@env-config @issue-6494
Scenario Outline: upload a file with virus by setting antivirus infected file handling config to continue
Given the config "ANTIVIRUS_INFECTED_FILE_HANDLING" has been set to "continue"
And using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/filesWithVirus/eicar.com" to "/aFileWithVirus.txt" using the WebDAV API
Then the HTTP status code should be "201"
And as "Alice" file "/aFileWithVirus.txt" should exist
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@env-config
Scenario Outline: upload a file with virus smaller than the upload threshold
Given the config "ANTIVIRUS_MAX_SCAN_SIZE" has been set to "100"
And using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/filesWithVirus/eicar.com" to "/aFileWithVirus.txt" using the WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| Virus found in aFileWithVirus.txt. Upload not possible. Virus: |
And as "Alice" file "/aFileWithVirus.txt" should not exist
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@env-config
Scenario Outline: upload a file with virus larger than the upload threshold
Given the config "ANTIVIRUS_MAX_SCAN_SIZE" has been set to "100"
And using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/filesWithVirus/eicar_com.zip" to "/aFileWithVirus.txt" using the WebDAV API
Then the HTTP status code should be "201"
And as "Alice" file "/aFileWithVirus.txt" should exist
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@issue-enterprise-5706 @issue-183 @issue-369
Scenario Outline: upload a file with virus and get notification in different languages
Given user "Alice" has switched the system language to "<language>" using the Graph API
And using <dav-path-version> DAV path
When user "Alice" uploads file "filesForUpload/filesWithVirus/eicar.com" to "/aFileWithVirus.txt" using the WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "<subject>" and message:
| message |
| <message> |
And as "Alice" file "/aFileWithVirus.txt" should not exist
Examples:
| dav-path-version | language | subject | message |
| old | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: |
| new | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: |
| spaces | es | Virus encontrado | Virus encontrado en aFileWithVirus.txt. La subida no ha sido posible. Virus: |
| old | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: |
| new | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: |
| spaces | de | Virus gefunden | In aFileWithVirus.txt wurde potenziell schädlicher Code gefunden. Das Hochladen wurde abgebrochen. Grund: |
@issue-enterprise-5709
Scenario Outline: try to create a version of file by uploading virus content
Given using <dav-path-version> DAV path
And user "Alice" has uploaded file with content "hello world" to "test.txt"
And user "Alice" has uploaded file with content "hello nepal" to "test.txt"
When user "Alice" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "test.txt" using the WebDAV API
Then the HTTP status code should be "204"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| Virus found in test.txt. Upload not possible. Virus: |
And as "Alice" file "/test.txt" should exist
And the version folder of file "/test.txt" for user "Alice" should contain "1" element
And the content of file "/test.txt" for user "Alice" should be "hello nepal"
When user "Alice" restores version index "1" of file "/test.txt" using the WebDAV API
Then the HTTP status code should be "204"
And the content of file "/test.txt" for user "Alice" should be "hello world"
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@env-config @issue-10331
Scenario Outline: try to overwrite a file with the virus content in a public link share
Given the config "OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD" has been set to "false"
And using <dav-path-version> DAV path
And using SharingNG
And user "Alice" has uploaded file with content "hello" to "test.txt"
And user "Alice" has created the following resource link share:
| resource | test.txt |
| space | Personal |
| permissionsRole | edit |
When the public overwrites file "test.txt" with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" using the public WebDAV API
Then the HTTP status code should be "204"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| Virus found in test.txt. Upload not possible. Virus: |
And the content of file "/test.txt" for user "Alice" should be "hello"
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: try to overwrite a file with the virus content in group share
Given using <dav-path-version> DAV path
And user "Brian" has been created with default attributes
And group "group1" has been created
And user "Brian" has been added to group "group1"
And user "Alice" has been added to group "group1"
And user "Alice" has uploaded file with content "hello" to "/test.txt"
And user "Alice" has sent the following resource share invitation:
| resource | test.txt |
| space | Personal |
| sharee | group1 |
| shareType | group |
| permissionsRole | File Editor |
And user "Brian" has a share "test.txt" synced
When user "Brian" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "Shares/test.txt" using the WebDAV API
Then the HTTP status code should be "204"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| Virus found in test.txt. Upload not possible. Virus: |
And the content of file "/test.txt" for user "Alice" should be "hello"
And the content of file "Shares/test.txt" for user "Brian" should be "hello"
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: try to overwrite a file with the virus content in user share
Given using <dav-path-version> DAV path
And user "Brian" has been created with default attributes
And user "Alice" has created folder "uploadFolder"
And user "Alice" has uploaded file with content "this is a test file." to "uploadFolder/test.txt"
And user "Alice" has sent the following resource share invitation:
| resource | uploadFolder |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Editor |
And user "Brian" has a share "uploadFolder" synced
And user "Alice" has uploaded file with content "this is a test file." to "/test.txt"
And user "Alice" has sent the following resource share invitation:
| resource | test.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | File Editor |
And user "Brian" has a share "test.txt" synced
When user "Brian" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "Shares/uploadFolder/test.txt" using the WebDAV API
Then the HTTP status code should be "204"
And user "Brian" should get a notification for resource "test.txt" with subject "Virus found" and message:
| message |
| Virus found in test.txt. Upload not possible. Virus: |
And the content of file "Shares/uploadFolder/test.txt" for user "Brian" should be "this is a test file."
And the content of file "uploadFolder/test.txt" for user "Alice" should be "this is a test file."
When user "Brian" uploads file with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to "Shares/test.txt" using the WebDAV API
Then the HTTP status code should be "204"
And user "Brian" should get a notification for resource "test.txt" with subject "Virus found" and message:
| message |
| Virus found in test.txt. Upload not possible. Virus: |
And the content of file "Shares/test.txt" for user "Brian" should be "this is a test file."
And the content of file "/test.txt" for user "Alice" should be "this is a test file."
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario: try to overwrite the .space/readme.md file
Given using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has created a folder ".space" in space "new-space"
And user "Alice" has uploaded a file inside space "new-space" with content "Here you can add a description for this Space." to ".space/readme.md"
When user "Alice" uploads a file inside space "new-space" with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to ".space/readme.md" using the WebDAV API
Then the HTTP status code should be "204"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| Virus found in readme.md. Upload not possible. Virus: |
And for user "Alice" the content of the file ".space/readme.md" of the space "new-space" should be "Here you can add a description for this Space."
Scenario: try to overwrite the .space/readme.md file in space share
Given using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Brian" has been created with default attributes
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Editor |
And user "Alice" has created a folder ".space" in space "new-space"
And user "Alice" has uploaded a file inside space "new-space" with content "Here you can add a description for this Space." to ".space/readme.md"
And user "Alice" has set the file ".space/readme.md" as a description in a special section of the "new-space" space
When user "Brian" uploads a file inside space "new-space" with content "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" to ".space/readme.md" using the WebDAV API
Then the HTTP status code should be "204"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| Virus found in readme.md. Upload not possible. Virus: |
And for user "Brian" the content of the file ".space/readme.md" of the space "new-space" should be "Here you can add a description for this Space."
And for user "Alice" the content of the file ".space/readme.md" of the space "new-space" should be "Here you can add a description for this Space."
Scenario: member of a project space tries to overwrite a file with virus content
Given using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Brian" has been created with default attributes
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Editor |
And user "Alice" has uploaded a file inside space "new-space" with content "hello world" to "text.txt"
When user "Brian" uploads a file "filesForUpload/filesWithVirus/eicar.com" to "text.txt" in space "new-space" using the WebDAV API
Then the HTTP status code should be "204"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| Virus found in text.txt. Upload not possible. Virus: |
And for user "Brian" the content of the file "/text.txt" of the space "new-space" should be "hello world"
And for user "Alice" the content of the file "/text.txt" of the space "new-space" should be "hello world"
@@ -0,0 +1,15 @@
Feature: service health check
Scenario: check service health
When a user requests these URLs with "GET" and no authentication
| endpoint | service |
| http://%base_url_hostname%:9297/healthz | antivirus |
Then the HTTP status code of responses on all endpoints should be "200"
Scenario: check service readiness
When a user requests these URLs with "GET" and no authentication
| endpoint | service |
| http://%base_url_hostname%:9297/readyz | antivirus |
Then the HTTP status code of responses on all endpoints should be "200"
@@ -0,0 +1,174 @@
Feature: download multiple resources bundled into an archive
As a user
I want to be able to download multiple items at once
So that I don't have to execute repetitive tasks
As a developer
I want to be able to use the resource ID to download multiple items at once
So that I don't have to know the full path of the resource
Background:
Given user "Alice" has been created with default attributes
Scenario Outline: download a single file
Given user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
When user "Alice" downloads the <archive-type> archive of "/textfile0.txt" using the resource id and setting these headers:
| header | value |
| User-Agent | <user-agent> |
Then the HTTP status code should be "200"
And the downloaded <archive-type> archive should contain these files:
| name | content |
| textfile0.txt | some data |
Examples:
| user-agent | archive-type |
| Linux | tar |
| Windows NT | zip |
Scenario Outline: download a single folder
Given user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile0.txt"
And user "Alice" has uploaded file with content "more data" to "/my_data/an_other_file.txt"
When user "Alice" downloads the <archive-type> archive of "/my_data" using the resource id and setting these headers:
| header | value |
| User-Agent | <user-agent> |
Then the HTTP status code should be "200"
And the downloaded <archive-type> archive should contain these files:
| name | content |
| my_data/textfile0.txt | some data |
| my_data/an_other_file.txt | more data |
Examples:
| user-agent | archive-type |
| Linux | zip |
| Windows NT | tar |
Scenario: download multiple files and folders
Given user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
And user "Alice" has uploaded file with content "other data" to "/textfile1.txt"
And user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile2.txt"
And user "Alice" has created folder "more_data"
And user "Alice" has uploaded file with content "more data" to "/more_data/an_other_file.txt"
When user "Alice" downloads the archive of these items using the resource ids
| textfile0.txt |
| textfile1.txt |
| my_data |
| more_data |
Then the HTTP status code should be "200"
And the downloaded zip archive should contain these files:
| name | content |
| textfile0.txt | some data |
| textfile1.txt | other data |
| my_data/textfile2.txt | some data |
| more_data/an_other_file.txt | more data |
Scenario: download a single file as different user
Given user "Brian" has been created with default attributes
And user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
When user "Brian" downloads the archive of "/textfile0.txt" of user "Alice" using the resource id
Then the HTTP status code should be "404"
Scenario: download multiple shared items as share receiver
Given user "Brian" has been created with default attributes
And user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
And user "Alice" has uploaded file with content "other data" to "/textfile1.txt"
And user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile2.txt"
And user "Alice" has created folder "more_data"
And user "Alice" has uploaded file with content "more data" to "/more_data/an_other_file.txt"
And user "Alice" has sent the following resource share invitation:
| resource | textfile0.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile0.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | textfile1.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile1.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | my_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "my_data" synced
And user "Alice" has sent the following resource share invitation:
| resource | more_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "more_data" synced
When user "Brian" downloads the archive of these items using the resource ids
| /Shares/textfile0.txt |
| /Shares/textfile1.txt |
| /Shares/my_data |
| /Shares/more_data |
Then the HTTP status code should be "200"
And the downloaded zip archive should contain these files:
| name | content |
| textfile0.txt | some data |
| textfile1.txt | other data |
| my_data/textfile2.txt | some data |
| more_data/an_other_file.txt | more data |
@issue-4636
Scenario Outline: download the Shares folder as share receiver
Given user "Brian" has been created with default attributes
And user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
And user "Alice" has uploaded file with content "other data" to "/textfile1.txt"
And user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile2.txt"
And user "Alice" has created folder "more_data"
And user "Alice" has uploaded file with content "more data" to "/more_data/an_other_file.txt"
And user "Alice" has sent the following resource share invitation:
| resource | textfile0.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile0.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | textfile1.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile1.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | my_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "my_data" synced
And user "Alice" has sent the following resource share invitation:
| resource | more_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "more_data" synced
When user "Brian" downloads the <archive-type> archive of "/Shares" using the resource id and setting these headers:
| header | value |
| User-Agent | <user-agent> |
Then the HTTP status code should be "200"
And the downloaded <archive-type> archive should contain these files:
| name | content |
| textfile0.txt | some data |
| textfile1.txt | other data |
| my_data/textfile2.txt | some data |
| more_data/an_other_file.txt | more data |
Examples:
| user-agent | archive-type |
| Linux | tar |
| Windows NT | zip |
@@ -0,0 +1,172 @@
Feature: download multiple resources bundled into an archive
As a user
I want to be able to download multiple items at once
So that I don't have to execute repetitive tasks
As a developer
I want to be able to use the full path of the resource to download multiple items at once
So that I don't have to know the ID of the resource
Background:
Given user "Alice" has been created with default attributes
@issue-4637
Scenario Outline: download a single file
Given user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
When user "Alice" downloads the <archive-type> archive of "/home/textfile0.txt" using the resource path and setting these headers:
| header | value |
| User-Agent | <user-agent> |
Then the HTTP status code should be "200"
And the downloaded <archive-type> archive should contain these files:
| name | content |
| textfile0.txt | some data |
Examples:
| user-agent | archive-type |
| Linux | tar |
| Windows NT | zip |
@issue-4637
Scenario Outline: download a single folder
Given user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile0.txt"
And user "Alice" has uploaded file with content "more data" to "/my_data/an_other_file.txt"
When user "Alice" downloads the <archive-type> archive of "/home/my_data" using the resource path and setting these headers:
| header | value |
| User-Agent | <user-agent> |
Then the HTTP status code should be "200"
And the downloaded <archive-type> archive should contain these files:
| name | content |
| my_data/textfile0.txt | some data |
| my_data/an_other_file.txt | more data |
Examples:
| user-agent | archive-type |
| Linux | tar |
| Windows NT | zip |
@issue-4637
Scenario: download multiple files and folders
Given user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
And user "Alice" has uploaded file with content "other data" to "/textfile1.txt"
And user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile2.txt"
And user "Alice" has created folder "more_data"
And user "Alice" has uploaded file with content "more data" to "/more_data/an_other_file.txt"
When user "Alice" downloads the archive of these items using the resource paths
| /home/textfile0.txt |
| /home/textfile1.txt |
| /home/my_data |
| /home/more_data |
Then the HTTP status code should be "200"
And the downloaded tar archive should contain these files:
| name | content |
| textfile0.txt | some data |
| textfile1.txt | other data |
| my_data/textfile2.txt | some data |
| more_data/an_other_file.txt | more data |
Scenario: download a not existing single file
When user "Alice" downloads the archive of "/doesnotexist.txt" of user "Alice" using the resource path
Then the HTTP status code should be "404"
@issue-4637
Scenario: download multiple shared items as share receiver
Given user "Brian" has been created with default attributes
And user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
And user "Alice" has uploaded file with content "other data" to "/textfile1.txt"
And user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile2.txt"
And user "Alice" has created folder "more_data"
And user "Alice" has uploaded file with content "more data" to "/more_data/an_other_file.txt"
And user "Alice" has sent the following resource share invitation:
| resource | textfile0.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile0.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | textfile1.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile1.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | my_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "my_data" synced
And user "Alice" has sent the following resource share invitation:
| resource | more_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "more_data" synced
When user "Brian" downloads the archive of these items using the resource path
| /home/Shares/textfile0.txt |
| /home/Shares/textfile1.txt |
| /home/Shares/my_data |
| /home/Shares/more_data |
Then the HTTP status code should be "200"
And the downloaded tar archive should contain these files:
| name | content |
| textfile0.txt | some data |
| textfile1.txt | other data |
| my_data/textfile2.txt | some data |
| more_data/an_other_file.txt | more data |
@issue-4637
Scenario Outline: download the Shares folder as share receiver
Given user "Brian" has been created with default attributes
And user "Alice" has uploaded file with content "some data" to "/textfile0.txt"
And user "Alice" has uploaded file with content "other data" to "/textfile1.txt"
And user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "some data" to "/my_data/textfile2.txt"
And user "Alice" has created folder "more_data"
And user "Alice" has uploaded file with content "more data" to "/more_data/an_other_file.txt"
And user "Alice" has sent the following resource share invitation:
| resource | textfile0.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile0.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | textfile1.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile1.txt" synced
And user "Alice" has sent the following resource share invitation:
| resource | my_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "my_data" synced
And user "Alice" has sent the following resource share invitation:
| resource | more_data |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "more_data" synced
When user "Brian" downloads the <archive-type> archive of "/home/Shares" using the resource path and setting these headers:
| header | value |
| User-Agent | <user-agent> |
Then the HTTP status code should be "200"
And the downloaded <archive-type> archive should contain these files:
| name | content |
| Shares/textfile0.txt | some data |
| Shares/textfile1.txt | other data |
| Shares/my_data/textfile2.txt | some data |
| Shares/more_data/an_other_file.txt | more data |
Examples:
| user-agent | archive-type |
| Linux | tar |
| Windows NT | zip |
@@ -0,0 +1,46 @@
@env-config
Feature: delay post-processing of uploaded files
As a user
I want to delay the post-processing of uploaded files
So that I can check the early request
Background:
Given user "Alice" has been created with default attributes
And async upload has been enabled with post-processing delayed to "30" seconds
@issue-5326
Scenario Outline: user sends GET request to the file while it's still being processed
Given user "Alice" has uploaded file with content "uploaded content" to "/file.txt"
When user "Alice" requests "<dav-path>" with "GET" without retrying
Then the HTTP status code should be "425"
Examples:
| dav-path |
| /webdav/file.txt |
| /dav/files/%username%/file.txt |
| /dav/spaces/%spaceid%/file.txt |
Scenario Outline: user sends PROPFIND request to the file while it's still being processed
Given user "Alice" has uploaded file with content "uploaded content" to "/file.txt"
When user "Alice" requests "<dav-path>" with "PROPFIND" without retrying
Then the HTTP status code should be "207"
And the value of the item "//d:response/d:propstat/d:status" in the response should be "HTTP/1.1 425 TOO EARLY"
Examples:
| dav-path |
| /webdav/file.txt |
| /dav/files/%username%/file.txt |
| /dav/spaces/%spaceid%/file.txt |
Scenario Outline: user sends PROPFIND request to the folder while files in the folder are still being processed
Given user "Alice" has created folder "my_data"
And user "Alice" has uploaded file with content "uploaded content" to "/my_data/file.txt"
When user "Alice" requests "<dav-path>" with "PROPFIND" without retrying
Then the HTTP status code should be "207"
And as user "Alice" the value of the item "//d:status" of path "<dav-path>/" in the response should be "HTTP/1.1 200 OK"
And as user "Alice" the value of the item "//d:status" of path "<dav-path>/file.txt" in the response should be "HTTP/1.1 425 TOO EARLY"
Examples:
| dav-path |
| /webdav/my_data |
| /dav/files/%username%/my_data |
| /dav/spaces/%spaceid%/my_data |
@@ -0,0 +1,131 @@
Feature: create auth-app token
As a user
I want to create auth-app Tokens
So that I can use 3rd party apps
Background:
Given user "Alice" has been created with default attributes
Scenario: user creates auth-app token
When user "Alice" creates app token with expiration time "72h" using the auth-app API
Then the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"token",
"expiration_date",
"created_date",
"label"
],
"properties": {
"token": {
"pattern": "^([a-zA-Z]+ ){5}[a-zA-Z]+$"
},
"label": {
"const": "Generated via API"
}
}
}
"""
@env-config
Scenario: user lists auth-app tokens generated by different auth-app api
Given the config "AUTH_APP_ENABLE_IMPERSONATION" has been set to "true"
And user "Alice" has created app token with expiration time "72h" using the auth-app API
And the administrator has created app token for user "Alice" with expiration time "72h" using the auth-app API
And user "Alice" has created app token with expiration time "72h" using the auth-app CLI
When user "Alice" lists all created tokens using the auth-app API
Then the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "array",
"minItems": 3,
"maxItems": 3,
"uniqueItems": true,
"items": {
"oneOf": [
{
"type": "object",
"required": [
"token",
"expiration_date",
"created_date",
"label"
],
"properties": {
"token": {
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$"
},
"label": {
"const": "Generated via API"
}
}
},
{
"type": "object",
"required": [
"token",
"expiration_date",
"created_date",
"label"
],
"properties": {
"token": {
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$"
},
"label": {
"const": "Generated via CLI"
}
}
},
{
"type": "object",
"required": [
"token",
"expiration_date",
"created_date",
"label"
],
"properties": {
"token": {
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$"
},
"label": {
"const": "Generated via API (Impersonation)"
}
}
}
]
}
}
"""
@env-config
Scenario: admin creates auth-app token for other user
Given the config "AUTH_APP_ENABLE_IMPERSONATION" has been set to "true"
When the administrator creates app token for user "Alice" with expiration time "72h" using the auth-app API
Then the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"token",
"expiration_date",
"created_date",
"label"
],
"properties": {
"token": {
"pattern": "^([a-zA-Z]+ ){5}[a-zA-Z]+$"
},
"label": {
"const": "Generated via API (Impersonation)"
}
}
}
"""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
Feature: service health check
Scenario: check service health
When a user requests these URLs with "GET" and no authentication
| endpoint | service |
| http://%collaboration_hostname%:9304/healthz | collaboration |
Then the HTTP status code of responses on all endpoints should be "200"
Scenario: check service readiness
When a user requests these URLs with "GET" and no authentication
| endpoint | service |
| http://%collaboration_hostname%:9304/readyz | collaboration |
Then the HTTP status code of responses on all endpoints should be "200"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
Feature: Copy test
As a user
I want to check the PROPFIND response
So that I can make sure that the response contains all the relevant values
Background:
Given these users have been created with default attributes:
| username |
| Alice |
And using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
Scenario: check the COPY response headers
Given user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testfile.txt"
And user "Alice" has created a folder "new" in space "new-space"
When user "Alice" copies file "testfile.txt" from space "new-space" to "/new/testfile.txt" inside space "new-space" with following headers using the WebDAV API
| header | value |
| Origin | %base_url% |
Then the HTTP status code should be "201"
And the following headers should match these regular expressions
| Oc-Fileid | /^[a-f0-9!\$\-]{110}$/ |
| Access-Control-Allow-Origin | /^%base_url%$/ |
| X-Request-Id | %request_id_pattern% |
@@ -0,0 +1,148 @@
Feature: Propfind test
As a user
I want to check the PROPFIND response
So that I can make sure that the response contains all the relevant values
Background:
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
And using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
Scenario: space-admin checks the PROPFIND request of a space
Given user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testfile.txt"
When user "Alice" sends PROPFIND request to space "new-space" with depth "0" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the PROPFIND response should contain a space "new-space" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:name | new-space |
| oc:permissions | RDNVCKZP |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 12 |
Scenario Outline: space member with a different role checks the PROPFIND request of a space
Given user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testfile.txt"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | <space-role> |
When user "Brian" sends PROPFIND request to space "new-space" with depth "0" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the PROPFIND response should contain a space "new-space" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:name | new-space |
| oc:permissions | <oc-permission> |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 12 |
Examples:
| space-role | oc-permission |
| Manager | RDNVCKZP |
| Space Editor | DNVCK |
| Space Viewer | |
Scenario Outline: space member with a different role checks the PROPFIND request of the folder in the space
Given user "Alice" has created a folder "folderMain" in space "new-space"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | <space-role> |
When user "Brian" sends PROPFIND request from the space "new-space" to the resource "folderMain" with depth "0" using the WebDAV API
Then the HTTP status code should be "207"
And as user "Brian" the PROPFIND response should contain a resource "folderMain" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain |
| oc:permissions | <oc-permission> |
| oc:size | 0 |
Examples:
| space-role | oc-permission |
| Manager | RDNVCKZP |
| Space Editor | DNVCK |
| Space Viewer | |
Scenario Outline: space member with a different role checks the PROPFIND request of the sub-folder in the space
Given user "Alice" has created a folder "folderMain/subFolder1/subFolder2" in space "new-space"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | <space-role> |
When user "Brian" sends PROPFIND request from the space "new-space" to the resource "folderMain/subFolder1/subFolder2" with depth "0" using the WebDAV API
Then the HTTP status code should be "207"
And as user "Brian" the PROPFIND response should contain a resource "subFolder2" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | subFolder2 |
| oc:permissions | <oc-permission> |
| oc:size | 0 |
Examples:
| space-role | oc-permission |
| Manager | RDNVCKZP |
| Space Editor | DNVCK |
| Space Viewer | |
Scenario Outline: space member with a different role checks the PROPFIND request of the file in the space
Given user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testfile.txt"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | <space-role> |
When user "Brian" sends PROPFIND request from the space "new-space" to the resource "testfile.txt" with depth "0" using the WebDAV API
Then the HTTP status code should be "207"
And as user "Brian" the PROPFIND response should contain a resource "testfile.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | testfile.txt |
| oc:permissions | <oc-permission> |
| oc:size | 12 |
Examples:
| space-role | oc-permission |
| Manager | RDNVWZP |
| Space Editor | DNVW |
| Space Viewer | |
@issue-1523
Scenario: propfind response contains a restored folder with correct name
Given user "Alice" has created a folder "folderMain" in space "Personal"
And user "Alice" has marked folder "folderMain" as favorite from space "Personal"
And user "Alice" has deleted folder "folderMain"
And user "Alice" has created a folder "folderMain" in space "Personal"
When user "Alice" restores the folder with original path "/folderMain" to "/folderMain (1)" using the trashbin API
And user "Alice" sends PROPFIND request to space "Personal" using the WebDAV API
Then the HTTP status code should be "207"
And as user "Alice" the PROPFIND response should contain a resource "folderMain" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain |
| oc:permissions | RDNVCKZP |
| oc:size | 0 |
| oc:favorite | 0 |
And as user "Alice" the PROPFIND response should contain a resource "folderMain (1)" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain (1) |
| oc:permissions | RDNVCKZP |
| oc:size | 0 |
| oc:favorite | 1 |
@@ -0,0 +1,181 @@
Feature: REPORT request to Shares space
As a user
I want to check the share REPORT response
So that I can make sure that the response contains all the relevant details for shares
Background:
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
And user "Alice" has created folder "/folderMain"
And user "Alice" has created folder "/folderMain/SubFolder1"
And user "Alice" has created folder "/folderMain/SubFolder1/subFOLDER2"
And user "Alice" has sent the following resource share invitation:
| resource | /folderMain |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "folderMain" synced
Scenario Outline: check the REPORT response of the found folder
Given using <dav-path-version> DAV path
When user "Brian" searches for "SubFolder1" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "SubFolder1" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | SubFolder1 |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: check the REPORT response of the found file
Given using <dav-path-version> DAV path
And user "Alice" has uploaded file with content "Not all those who wander are lost." to "/folderMain/SubFolder1/subFOLDER2/frodo.txt"
And user "Alice" has uploaded file "filesForUpload/testavatar.jpg" to "/folderMain/SubFolder1/subFOLDER2/testavatar.jpg"
When user "Brian" searches for "frodo.txt" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "frodo.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | frodo.txt |
| d:getcontenttype | text/plain |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 34 |
| oc:remote-item-id | %file_id_pattern% |
When user "Brian" searches for "testavatar.jpg" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "testavatar.jpg" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | testavatar.jpg |
| d:getcontenttype | image/jpeg |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:has-preview | 1 |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: search for the shared folder when share is not accepted
Given user "Brian" has disabled auto-accepting
And using <dav-path-version> DAV path
And user "Alice" has created folder "/folderToBrian"
And user "Alice" has sent the following resource share invitation:
| resource | /folderToBrian |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
When user "Brian" searches for "folderToBrian" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And the search result should contain "0" entries
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@issue-9607 @issue-10329 @env-config
Scenario Outline: check the REPORT response of a folder shared with secure viewer role
Given using <dav-path-version> DAV path
And the administrator has enabled the permissions role "Secure Viewer"
And user "Alice" has created folder "/secureFolder"
And user "Alice" has uploaded file with content "secure content" to "/secureFolder/secure.txt"
And user "Alice" has sent the following resource share invitation:
| resource | secureFolder |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Secure Viewer |
And user "Brian" has a share "secureFolder" synced
When user "Brian" searches for "secureFolder" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "secureFolder" with these key and value pairs:
| key | value |
| oc:shareroot | /secureFolder |
| oc:name | secureFolder |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | SMX |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 14 |
| oc:remote-item-id | %file_id_pattern% |
When user "Brian" searches for "secure.txt" using the WebDAV API
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
Then the HTTP status code should be "207"
And as user "Brian" the REPORT response should contain a resource "secure.txt" with these key and value pairs:
| key | value |
| oc:shareroot | /secureFolder |
| oc:name | secure.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SX |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 14 |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@issue-9607 @issue-10329 @env-config
Scenario Outline: check the REPORT response of a file shared with secure viewer role
Given using <dav-path-version> DAV path
And the administrator has enabled the permissions role "Secure Viewer"
And user "Alice" has uploaded file with content "secure content" to "/secure.txt"
And user "Alice" has sent the following resource share invitation:
| resource | secure.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Secure Viewer |
And user "Brian" has a share "secure.txt" synced
When user "Brian" searches for "secure.txt" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "secure.txt" with these key and value pairs:
| key | value |
| oc:shareroot | /secure.txt |
| oc:name | secure.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SMX |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 14 |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@@ -0,0 +1,95 @@
Feature: REPORT request to project space
As a user
I want to check the REPORT response of project spaces
So that I can make sure that the response contains all the relevant details
Background:
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
And using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "findData" with the default quota using the Graph API
@issue-10329
Scenario: check the response of the searched file
Given user "Alice" has uploaded a file inside space "findData" with content "some content" to "testFile.txt"
And user "Alice" has marked file "testFile.txt" as favorite from space "findData"
When user "Alice" searches for "testFile.txt" using the WebDAV API
Then the HTTP status code should be "207"
And the search result of user "Alice" should contain only these entries:
| testFile.txt |
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the REPORT response should contain a resource "testFile.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | testFile.txt |
| d:getcontenttype | text/plain |
| oc:permissions | RDNVW |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 12 |
| oc:favorite | 1 |
@issue-10329
Scenario: check the response of the searched sub-file
Given user "Alice" has created a folder "folderMain/SubFolder1/subFOLDER2" in space "findData"
And user "Alice" has uploaded a file inside space "findData" with content "some content" to "folderMain/SubFolder1/subFOLDER2/insideTheFolder.txt"
When user "Alice" searches for "insideTheFolder.txt" using the WebDAV API
Then the HTTP status code should be "207"
And the search result of user "Alice" should contain only these entries:
| insideTheFolder.txt |
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the REPORT response should contain a resource "insideTheFolder.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | insideTheFolder.txt |
| d:getcontenttype | text/plain |
| oc:permissions | RDNVW |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 12 |
@issue-10329
Scenario: check the response of the searched folder
Given user "Alice" has created a folder "folderMain" in space "findData"
And user "Alice" has marked folder "folderMain" as favorite from space "findData"
When user "Alice" searches for "folderMain" using the WebDAV API
Then the HTTP status code should be "207"
And the search result of user "Alice" should contain only these entries:
| folderMain |
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the REPORT response should contain a resource "folderMain" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | RDNVCK |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 0 |
| oc:favorite | 1 |
@issue-10329
Scenario: check the response of the searched sub-folder
Given user "Alice" has created a folder "folderMain/sub-folder" in space "findData"
When user "Alice" searches for "*sub*" using the WebDAV API
Then the HTTP status code should be "207"
And the search result of user "Alice" should contain only these entries:
| sub-folder |
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And the HTTP status code should be "207"
And as user "Alice" the REPORT response should contain a resource "sub-folder" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | sub-folder |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | RDNVCK |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 0 |
@@ -0,0 +1,98 @@
Feature: Report test
As a user
I want to check the share REPORT response
So that I can make sure that the response contains all the relevant details for shares
Background:
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
And using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "find data" with the default quota using the Graph API
And user "Alice" has created a folder "folderMain/SubFolder1/subFOLDER2" in space "find data"
And user "Alice" has uploaded a file inside space "find data" with content "some content" to "folderMain/SubFolder1/subFOLDER2/insideTheFolder.txt"
Scenario Outline: check the response of the found folder
Given using <dav-path-version> DAV path
And user "Alice" has sent the following resource share invitation:
| resource | folderMain |
| space | find data |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "folderMain" synced
When user "Brian" searches for "SubFolder1" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "SubFolder1" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | SubFolder1 |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 12 |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: check the response of the found file
Given using <dav-path-version> DAV path
And user "Alice" has sent the following resource share invitation:
| resource | folderMain |
| space | find data |
| sharee | Brian |
| shareType | user |
| permissionsRole | Editor |
And user "Brian" has a share "folderMain" synced
When user "Brian" searches for "insideTheFolder.txt" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "insideTheFolder.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | insideTheFolder.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SD |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 12 |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: search for the shared folder when the share is not accepted
Given using <dav-path-version> DAV path
And user "Brian" has disabled auto-accepting
And user "Alice" has sent the following resource share invitation:
| resource | folderMain |
| space | find data |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
When user "Brian" searches for "folderMain" using the WebDAV API
Then the HTTP status code should be "207"
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And the search result should contain "0" entries
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@@ -0,0 +1,168 @@
# NOTE: set env OC_CORS_ALLOW_ORIGINS=https://aphno.badal while running КуСфера server
@env-config
Feature: CORS headers
As a user
I want to send a cross-origin request
So that I can check if the correct headers are set
Background:
Given user "Alice" has been created with default attributes
And the config "OC_CORS_ALLOW_ORIGINS" has been set to "https://aphno.badal"
@issue-5195
Scenario Outline: CORS headers should be returned when setting CORS domain sending origin header
Given using OCS API version "<ocs-api-version>"
When user "Alice" sends HTTP method "GET" to OCS API endpoint "<endpoint>" with headers
| header | value |
| Origin | https://aphno.badal |
Then the OCS status code should be "<ocs-status-code>"
And the HTTP status code should be "<http-status-code>"
And the following headers should be set
| header | value |
| Access-Control-Expose-Headers | Location |
| Access-Control-Allow-Origin | https://aphno.badal |
Examples:
| ocs-api-version | endpoint | ocs-status-code | http-status-code |
| 1 | /config | 100 | 200 |
| 2 | /config | 200 | 200 |
| 1 | /apps/files_sharing/api/v1/shares | 100 | 200 |
| 2 | /apps/files_sharing/api/v1/shares | 200 | 200 |
Scenario Outline: CORS headers should not be returned when CORS domain does not match origin header
Given using OCS API version "<ocs-api-version>"
When user "Alice" sends HTTP method "GET" to OCS API endpoint "<endpoint>" with headers
| header | value |
| Origin | https://mero.badal |
Then the OCS status code should be "<ocs-status-code>"
And the HTTP status code should be "<http-status-code>"
And the following headers should not be set
| header |
| Access-Control-Allow-Headers |
| Access-Control-Expose-Headers |
| Access-Control-Allow-Origin |
| Access-Control-Allow-Methods |
Examples:
| ocs-api-version | endpoint | ocs-status-code | http-status-code |
| 1 | /config | 100 | 200 |
| 2 | /config | 200 | 200 |
| 1 | /apps/files_sharing/api/v1/shares | 100 | 200 |
| 2 | /apps/files_sharing/api/v1/shares | 200 | 200 |
@issue-5194
# The Access-Control-Request-Headers need to be in lower-case and alphabetically order to comply with the rs/cors
# package see: https://github.com/rs/cors/commit/4c32059b2756926619f6bf70281b91be7b5dddb2#diff-bf80d8fbedf172fab9ba2604da7f7be972e48b2f78a8d0cd21619d5f93665895R367
Scenario Outline: CORS headers should be returned when an preflight request is sent
Given using OCS API version "<ocs-api-version>"
When user "Alice" sends HTTP method "OPTIONS" to OCS API endpoint "<endpoint>" with headers
| header | value |
| Origin | https://aphno.badal |
| Access-Control-Request-Headers | accept,authorization,cache-control,content-type,depth,destination,if-match,if-none-match,ocs-apirequest,origin,overwrite,tus-checksum-algorithm,tus-resumable,upload-checksum,upload-concat,upload-defer-length,upload-expires,upload-length,upload-metadata,upload-offset,x-http-method-override,x-request-id,x-requested-with |
| Access-Control-Request-Method | <request-method> |
And the HTTP status code should be "204"
And the following headers should be set
| header | value |
| Access-Control-Allow-Headers | accept,authorization,cache-control,content-type,depth,destination,if-match,if-none-match,ocs-apirequest,origin,overwrite,tus-checksum-algorithm,tus-resumable,upload-checksum,upload-concat,upload-defer-length,upload-expires,upload-length,upload-metadata,upload-offset,x-http-method-override,x-request-id,x-requested-with |
| Access-Control-Allow-Origin | https://aphno.badal |
| Access-Control-Allow-Methods | <request-method> |
Examples:
| ocs-api-version | endpoint | request-method |
| 1 | /apps/files_sharing/api/v1/shares | GET |
| 2 | /apps/files_sharing/api/v1/shares | PUT |
| 1 | /apps/files_sharing/api/v1/shares | DELETE |
| 2 | /apps/files_sharing/api/v1/shares | POST |
Scenario: CORS headers should be returned when setting CORS domain sending origin header in the Graph api
When user "Alice" lists all available spaces with headers using the Graph API
| header | value |
| Origin | https://aphno.badal |
Then the HTTP status code should be "200"
And the following headers should be set
| header | value |
| Access-Control-Allow-Origin | https://aphno.badal |
@issue-8231
Scenario Outline: CORS headers should be returned when setting CORS domain sending origin header in the Webdav api
Given using <dav-path-version> DAV path
When user "Alice" sends PROPFIND request to space "Personal" with headers using the WebDAV API
| header | value |
| Origin | https://aphno.badal |
Then the HTTP status code should be "207"
And the following headers should be set
| header | value |
| Access-Control-Allow-Origin | https://aphno.badal |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario: CORS headers should be returned when setting CORS domain sending origin header in the settings api
When user "Alice" lists values-list with headers using the Settings API
| header | value |
| Origin | https://aphno.badal |
Then the HTTP status code should be "201"
And the following headers should be set
| header | value |
| Access-Control-Allow-Origin | https://aphno.badal |
@issue-8380
Scenario: CORS headers should be returned when uploading file using Tus and when CORS domain sending origin header in the Webdav api
Given user "Alice" has created a new TUS resource in the space "Personal" with the following headers:
| Upload-Length | 5 |
# dGV4dEZpbGUudHh0 is the base64 encode of textFile.txt
| Upload-Metadata | filename dGV4dEZpbGUudHh0 |
| Tus-Resumable | 1.0.0 |
| Origin | https://aphno.badal |
When user "Alice" sends a chunk to the last created TUS Location with data "01234" with the following headers:
| Origin | https://aphno.badal |
| Upload-Checksum | MD5 4100c4d44da9177247e44a5fc1546778 |
| Upload-Offset | 0 |
Then the HTTP status code should be "204"
And the following headers should be set
| header | value |
| Access-Control-Allow-Origin | https://aphno.badal |
And for user "Alice" the content of the file "/textFile.txt" of the space "Personal" should be "01234"
@issue-8380
Scenario: uploading file using Tus using different CORS headers
Given user "Alice" has created a new TUS resource in the space "Personal" with the following headers:
| Upload-Length | 5 |
# dGV4dEZpbGUudHh0 is the base64 encode of textFile.txt
| Upload-Metadata | filename dGV4dEZpbGUudHh0 |
| Tus-Resumable | 1.0.0 |
| Origin | https://something.else |
When user "Alice" sends a chunk to the last created TUS Location with data "01234" with the following headers:
| Origin | https://something.else |
| Upload-Checksum | MD5 4100c4d44da9177247e44a5fc1546778 |
| Upload-Offset | 0 |
Then the HTTP status code should be "403"
@issue-8380
# The Access-Control-Request-Headers need to be in lower-case and alphabetically order to comply with the rs/cors
# package see: https://github.com/rs/cors/commit/4c32059b2756926619f6bf70281b91be7b5dddb2#diff-bf80d8fbedf172fab9ba2604da7f7be972e48b2f78a8d0cd21619d5f93665895R367
Scenario Outline: CORS headers should be returned when an preflight request is sent to Tus upload
Given user "Alice" has created a new TUS resource in the space "Personal" with the following headers:
| Upload-Length | 5 |
# dGV4dEZpbGUudHh0 is the base64 encode of textFile.txt
| Upload-Metadata | filename dGV4dEZpbGUudHh0 |
| Tus-Resumable | 1.0.0 |
When user "Alice" sends HTTP method "OPTIONS" to URL "<endpoint>" with headers
| header | value |
| Origin | https://aphno.badal |
| Access-Control-Request-Headers | accept,authorization,cache-control,content-type,depth,destination,if-match,if-none-match,ocs-apirequest,origin,overwrite,tus-checksum-algorithm,tus-resumable,upload-checksum,upload-concat,upload-defer-length,upload-expires,upload-length,upload-metadata,upload-offset,x-http-method-override,x-request-id,x-requested-with |
| Access-Control-Request-Method | <request-method> |
And the HTTP status code should be "204"
And the following headers should be set
| header | value |
| Access-Control-Allow-Headers | accept,authorization,cache-control,content-type,depth,destination,if-match,if-none-match,ocs-apirequest,origin,overwrite,tus-checksum-algorithm,tus-resumable,upload-checksum,upload-concat,upload-defer-length,upload-expires,upload-length,upload-metadata,upload-offset,x-http-method-override,x-request-id,x-requested-with |
| Access-Control-Allow-Origin | https://aphno.badal |
| Access-Control-Allow-Methods | <request-method> |
Examples:
| endpoint | request-method |
| /%tus_upload_location% | PUT |
| /%tus_upload_location% | POST |
| /%tus_upload_location% | HEAD |
| /%tus_upload_location% | PATCH |
@@ -0,0 +1,151 @@
@env-config
Feature: PROPFIND with depth:infinity
As a user
I want to retrieve all properties of a resource
So that I can get the information about a resource
Background:
Given the config "OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY" has been set to "true"
And user "Alice" has been created with default attributes
And user "Alice" has created the following folders
| path |
| simple-folder |
| simple-folder/simple-folder1 |
| simple-folder/simple-empty-folder |
| simple-folder/simple-folder1/simple-folder2 |
And user "Alice" has uploaded the following files with content "simple-test-content"
| path |
| textfile0.txt |
| welcome.txt |
| simple-folder/textfile0.txt |
| simple-folder/welcome.txt |
| simple-folder/simple-folder1/textfile0.txt |
| simple-folder/simple-folder1/welcome.txt |
| simple-folder/simple-folder1/simple-folder2/textfile0.txt |
| simple-folder/simple-folder1/simple-folder2/welcome.txt |
Scenario Outline: get the list of resources with depth infinity
Given using <dav-path-version> DAV path
When user "Alice" lists the resources in "/" with depth "infinity" using the WebDAV API
Then the HTTP status code should be "207"
And the last DAV response for user "Alice" should contain these nodes
| name |
| textfile0.txt |
| welcome.txt |
| simple-folder/ |
| simple-folder/textfile0.txt |
| simple-folder/welcome.txt |
| simple-folder/simple-empty-folder/ |
| simple-folder/simple-folder1/ |
| simple-folder/simple-folder1/simple-folder2 |
| simple-folder/simple-folder1/textfile0.txt |
| simple-folder/simple-folder1/welcome.txt |
| simple-folder/simple-folder1/simple-folder2/textfile0.txt |
| simple-folder/simple-folder1/simple-folder2/welcome.txt |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: get the list of resources of a folder with depth infinity
Given using <dav-path-version> DAV path
When user "Alice" lists the resources in "simple-folder" with depth "infinity" using the WebDAV API
Then the HTTP status code should be "207"
And the last DAV response for user "Alice" should contain these nodes
| name |
| simple-folder/textfile0.txt |
| simple-folder/welcome.txt |
| simple-folder/simple-folder1/ |
| simple-folder/simple-folder1/simple-folder2 |
| simple-folder/simple-folder1/textfile0.txt |
| simple-folder/simple-folder1/welcome.txt |
| simple-folder/simple-folder1/simple-folder2/textfile0.txt |
| simple-folder/simple-folder1/simple-folder2/welcome.txt |
Examples:
| dav-path-version |
| old |
| new |
| spaces |
@issue-10331
Scenario: get the list of resources in a folder shared through public link with depth infinity
Given using SharingNG
And the following configs have been set:
| config | value |
| OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY | true |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
And user "Alice" has created the following resource link share:
| resource | simple-folder |
| space | Personal |
| permissionsRole | view |
When the public lists the resources in the last created public link with depth "infinity" using the WebDAV API
Then the HTTP status code should be "207"
And the last public link DAV response should contain these nodes
| name |
| textfile0.txt |
| welcome.txt |
| simple-folder1/ |
| simple-folder1/welcome.txt |
| simple-folder1/simple-folder2 |
| simple-folder1/textfile0.txt |
| simple-folder1/simple-folder2/textfile0.txt |
| simple-folder1/simple-folder2/welcome.txt |
Scenario Outline: get the list of files in the trashbin with depth infinity
Given using <dav-path-version> DAV path
And user "Alice" has deleted the following resources
| path |
| textfile0.txt |
| welcome.txt |
| simple-folder/ |
When user "Alice" lists the resources in the trashbin with depth "infinity" using the WebDAV API
Then the HTTP status code should be "207"
And the trashbin DAV response should contain these nodes
| name |
| textfile0.txt |
| welcome.txt |
| simple-folder/ |
| simple-folder/textfile0.txt |
| simple-folder/welcome.txt |
| simple-folder/simple-folder1/textfile0.txt |
| simple-folder/simple-folder1/welcome.txt |
| simple-folder/simple-folder1/simple-folder2/textfile0.txt |
| simple-folder/simple-folder1/simple-folder2/welcome.txt |
Examples:
| dav-path-version |
| new |
| spaces |
@issue-10331
Scenario: get the list of resources in a folder shared through public link with depth infinity when depth infinity is not allowed
Given the following configs have been set:
| config | value |
| OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY | false |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
And using SharingNG
And user "Alice" has created the following resource link share:
| resource | simple-folder |
| space | Personal |
| permissionsRole | view |
When the public lists the resources in the last created public link with depth "infinity" using the WebDAV API
Then the HTTP status code should be "400"
Scenario Outline: get the list of files in the trashbin with depth infinity when depth infinity is not allowed
Given the config "OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY" has been set to "false"
And using <dav-path-version> DAV path
And user "Alice" has deleted the following resources
| path |
| textfile0.txt |
| welcome.txt |
| simple-folder |
When user "Alice" lists the resources in the trashbin with depth "infinity" using the WebDAV API
Then the HTTP status code should be "400"
Examples:
| dav-path-version |
| new |
| spaces |
@@ -0,0 +1,66 @@
Feature: Download file in project space
As a user with different role
I want to be able to download files
So that I can have it in my local storage
Background:
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
| Bob |
And using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "download file" with the default quota using the Graph API
And user "Alice" has uploaded a file inside space "download file" with content "some content" to "file.txt"
And user "Alice" has sent the following space share invitation:
| space | download file |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Editor |
And user "Alice" has sent the following space share invitation:
| space | download file |
| sharee | Bob |
| shareType | user |
| permissionsRole | Space Viewer |
Scenario Outline: user downloads a file in the project space
When user "<user>" downloads the file "file.txt" of the space "download file" using the WebDAV API
Then the HTTP status code should be "200"
And the following headers should be set
| header | value |
| Content-Length | 12 |
Examples:
| user |
| Alice |
| Brian |
| Bob |
Scenario Outline: users with role manager and editor can download an old version of the file in the project space
Given user "Alice" has uploaded a file inside space "download file" with content "new content" to "file.txt"
And user "Alice" has uploaded a file inside space "download file" with content "newest content" to "file.txt"
When user "<user>" downloads version of the file "file.txt" with the index "1" of the space "download file" using the WebDAV API
Then the HTTP status code should be "200"
And the following headers should be set
| header | value |
| Content-Length | 11 |
When user "<user>" downloads version of the file "file.txt" with the index "2" of the space "download file" using the WebDAV API
Then the HTTP status code should be "200"
And the following headers should be set
| header | value |
| Content-Length | 12 |
Examples:
| user |
| Alice |
| Brian |
Scenario: user with role viewer cannot get versions of a file in the project space
Given user "Alice" has uploaded a file inside space "download file" with content "new content" to "file.txt"
And user "Alice" has uploaded a file inside space "download file" with content "newest content" to "file.txt"
When user "Bob" tries to get versions of the file "file.txt" from the space "download file" using the WebDAV API
Then the HTTP status code should be "403"
When user "Bob" tries to download version of the file "file.txt" with the index "1" of the space "download file" using the WebDAV API
Then the HTTP status code should be "403"
@@ -0,0 +1,63 @@
Feature: Download space
As a user
I want to download space
So that I can store it locally
Background:
Given these users have been created with default attributes:
| username |
| Alice |
| Brian |
And using spaces DAV path
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "Project-space" with the default quota using the Graph API
And user "Alice" has uploaded a file inside space "Project-space" with content "some data" to "file1.txt"
And user "Alice" has created a folder ".space" in space "Project-space"
And user "Alice" has uploaded a file inside space "Project-space" with content "space description" to ".space/readme.md"
Scenario: user downloads a space
Given user "Alice" has uploaded a file inside space "Project-space" with content "other data" to "file2.txt"
When user "Alice" downloads the space "Project-space" using the WebDAV API
Then the HTTP status code should be "200"
And the downloaded "zip" archive should contain these files:
| name | content |
| file1.txt | some data |
| file2.txt | other data |
| .space/readme.md | space description |
Scenario Outline: user downloads a shared space (shared by others)
Given user "Alice" has sent the following space share invitation:
| space | Project-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | <space-role> |
When user "Brian" downloads the space "Project-space" using the WebDAV API
Then the HTTP status code should be "200"
And the downloaded "zip" archive should contain these files:
| name | content |
| file1.txt | some data |
| .space/readme.md | space description |
Examples:
| space-role |
| Manager |
| Space Editor |
| Space Viewer |
Scenario Outline: admin/space-admin tries to download a space that they do not have access to
Given the administrator has assigned the role "<user-role>" to user "Brian" using the Graph API
When user "Brian" tries to download the space "Project-space" owned by user "Alice" using the WebDAV API
Then the HTTP status code should be "404"
Examples:
| user-role |
| Admin |
| Space Admin |
Scenario: user tries to download disabled space
Given user "Alice" has disabled a space "Project-space"
When user "Alice" tries to download the space "Project-space" using the WebDAV API
Then the HTTP status code should be "404"
@@ -0,0 +1,72 @@
Feature: change role
As an admin
I want to change the role of user
So that I can manage the role of user
Background:
Given user "Alice" has been created with default attributes
Scenario Outline: admin user changes the role of another user with different roles
Given user "Brian" has been created with default attributes
And the administrator has assigned the role "Admin" to user "Alice" using the Graph API
And the administrator has assigned the role "<user-role>" to user "Brian" using the Graph API
When user "Alice" changes the role of user "Brian" to role "<new-user-role>" using the Graph API
Then the HTTP status code should be "201"
And user "Brian" should have the role "<new-user-role>"
Examples:
| user-role | new-user-role |
| Admin | Admin |
| Admin | Space Admin |
| Admin | User |
| Admin | User Light |
| Space Admin | Admin |
| Space Admin | Space Admin |
| Space Admin | User |
| Space Admin | User Light |
| User | Admin |
| User | Space Admin |
| User | User |
| User | User Light |
| User Light | Admin |
| User Light | Space Admin |
| User Light | User |
| User Light | User Light |
Scenario Outline: admin user tries to change his/her own role
Given the administrator has assigned the role "Admin" to user "Alice" using the Graph API
When user "Alice" tries to change the role of user "Alice" to role "<new-user-role>" using the Graph API
Then the HTTP status code should be "403"
And user "Alice" should have the role "Admin"
Examples:
| new-user-role |
| Space Admin |
| User |
| User Light |
| Admin |
Scenario Outline: non-admin cannot change the user role
Given the administrator has assigned the role "<user-role>" to user "Alice" using the Graph API
And user "Brian" has been created with default attributes
When user "Alice" tries to change the role of user "Alice" to role "Admin" using the Graph API
Then the HTTP status code should be "403"
And user "Brian" should have the role "User"
Examples:
| user-role |
| Space Admin |
| User |
| User Light |
Scenario Outline: non-admin user tries to change the role of nonexistent user
Given the administrator has assigned the role "<user-role>" to user "Alice" using the Graph API
And user "Brian" has been created with default attributes
When user "Alice" tries to change the role of user "nonexistent" to role "Admin" using the Graph API
Then the HTTP status code should be "403"
Examples:
| user-role |
| Space Admin |
| User |
| User Light |
@@ -0,0 +1,405 @@
@env-config
Feature: enforce password on public link
As a user
I want to enforce passwords on public links shared with upload, edit, or contribute permission
So that the password is required to access the contents of the link
Password requirements. set by default:
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | true |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 8 |
| OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 1 |
| OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 1 |
| OC_PASSWORD_POLICY_MIN_DIGITS | 1 |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 1 |
Background:
And user "Alice" has been created with default attributes
And user "Alice" has uploaded file with content "test file" to "/testfile.txt"
Scenario Outline: create a public link without a password when enforce-password for writable share is enabled
Given the following configs have been set:
| config | value |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
| OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD | true |
When user "Alice" creates the following resource link share using the Graph API:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | <permissions-role> |
Then the HTTP status code should be "<status-code>"
Examples:
| permissions-role | status-code |
| view | 200 |
| edit | 400 |
Scenario: try to update a public link to edit permission without a password
Given the following configs have been set:
| config | value |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
| OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD | true |
And user "Alice" has created the following resource link share:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | view |
When user "Alice" tries to update the last public link share using the permissions endpoint of the Graph API:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | edit |
Then the HTTP status code should be "400"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "string",
"pattern": "invalidRequest"
},
"message": {
"const": "password protection is enforced"
}
}
}
}
}
"""
@issue-2048
Scenario: update a public link to edit permission. Need set pasword first
Given the following configs have been set:
| config | value |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
| OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD | true |
And user "Alice" has created the following resource link share:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | view |
When user "Alice" sets the following password for the last link share using the Graph API:
| resource | testfile.txt |
| space | Personal |
| password | %public% |
Then the HTTP status code should be "200"
And user "Alice" updates the last public link share using the permissions endpoint of the Graph API:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | edit |
And the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"hasPassword",
"link"
],
"properties": {
"hasPassword": { "const": true },
"link": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": { "const": "edit" }
}
}
}
}
"""
@issue-9724 @issue-10331
Scenario: create a public link with a password in accordance with the password policy
Given the following configs have been set:
| config | value |
| OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD | true |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 13 |
| OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 3 |
| OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 2 |
| OC_PASSWORD_POLICY_MIN_DIGITS | 2 |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 2 |
When user "Alice" creates the following resource link share using the Graph API:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | edit |
| password | 3s:5WW9uE5h=A |
Then the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"hasPassword",
"id",
"link"
],
"properties": {
"hasPassword": { "const": true },
"id": {
"type": "string",
"pattern": "^[a-zA-Z]{15}$"
},
"link": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": { "const": "edit" }
}
}
}
}
"""
Scenario: try to create a public link with a password that does not comply with the password policy
Given the following configs have been set:
| config | value |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 13 |
| OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 3 |
| OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 2 |
| OC_PASSWORD_POLICY_MIN_DIGITS | 2 |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 2 |
When user "Alice" tries to create the following resource link share using the Graph API:
| space | Personal |
| resource | testfile.txt |
| permissionsRole | edit |
| password | Pas1 |
Then the HTTP status code should be "400"
And the JSON data of the response should match
"""
{
"type": "object",
"required": ["error"],
"properties": {
"error": {
"type": "object",
"required": [
"code",
"innererror",
"message"
],
"properties": {
"code": { "const": "invalidRequest" },
"innererror": {
"type": "object",
"required": [
"date",
"request-id"
]
},
"message": {
"type": "string",
"pattern": "at least 13 characters are required\\s+at least 3 lowercase letters are required\\s+at least 2 uppercase letters are required\\s+at least 2 numbers are required\\s+at least 2 special characters are required\\s+[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~]+"
}
}
}
}
}
"""
@issue-9724 @issue-10331
Scenario: update a public link with a password in accordance with the password policy
Given the following configs have been set:
| config | value |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
| OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD | true |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 13 |
| OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 3 |
| OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 2 |
| OC_PASSWORD_POLICY_MIN_DIGITS | 1 |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 2 |
And user "Alice" has created the following resource link share:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | view |
When user "Alice" sets the following password for the last link share using the Graph API:
| resource | testfile.txt |
| space | Personal |
| password | 6a0Q;A3 +i^m[ |
Then the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [ "hasPassword" ],
"properties": {
"hasPassword": { "const": true }
}
}
"""
Scenario Outline: try to update a public link with a password that does not comply with the password policy
Given the following configs have been set:
| config | value |
| OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD | false |
| OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD | true |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 13 |
| OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 3 |
| OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 2 |
| OC_PASSWORD_POLICY_MIN_DIGITS | 1 |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 2 |
And using OCS API version "<ocs-api-version>"
And using SharingNG
And user "Alice" has created the following resource link share:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | view |
When user "Alice" updates the last public link share using the sharing API with
| permissions | 3 |
| password | Pws^ |
And user "Alice" tries to set the following password for the last link share using the Graph API:
| resource | testfile.txt |
| space | Personal |
| password | Pws^ |
Then the HTTP status code should be "400"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [ "error" ],
"properties": {
"error": {
"type": "object",
"required": [ "message" ],
"properties": {
"message": {
"type": "string",
"pattern": "at least 13 characters are required\\s+at least 3 lowercase letters are required\\s+at least 2 uppercase letters are required\\s+at least 1 numbers are required\\s+at least 2 special characters are required\\s+[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~]+"
}
}
}
}
}
"""
@issue-9724 @issue-10331
Scenario Outline: create a public link with a password in accordance with the password policy (valid cases)
Given the config "<config>" has been set to "<config-value>"
When user "Alice" creates the following resource link share using the Graph API:
| space | Personal |
| resource | testfile.txt |
| permissionsRole | view |
| password | <password> |
Then the HTTP status code should be "200"
Examples:
| config | config-value | password |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 4 | Ps-1 |
| OC_PASSWORD_POLICY_MIN_CHARACTERS | 14 | Ps1:with space |
| OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 4 | PS1:test |
| OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 3 | PS1:TeƒsT |
| OC_PASSWORD_POLICY_MIN_DIGITS | 2 | PS1:test2 |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 2 | PS1:test pass |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 33 | pS1! #$%&'()*+,-./:;<=>?@[\]^_`{ }~ |
| OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS | 5 | 1sameCharacterShouldWork!!!!! |
Scenario Outline: try to create a public link with a password that does not comply with the password policy (invalid cases)
When user "Alice" tries to create the following resource link share using the Graph API:
| space | Personal |
| resource | testfile.txt |
| permissionsRole | view |
| password | <password> |
Then the HTTP status code should be "400"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [ "error" ],
"properties": {
"error": {
"type": "object",
"required": [ "message" ],
"properties": {
"message": {
"const": "<message>"
}
}
}
}
}
"""
Examples:
| password | message |
| 1Pw: | at least 8 characters are required |
| 1P:12345 | at least 1 lowercase letters are required |
| test-123 | at least 1 uppercase letters are required |
| Test-psw | at least 1 numbers are required |
Scenario Outline: update a public link with a password that is listed in the Banned-Password-List
Given the config "OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST" has been set to path "config/woodpecker/banned-password-list.txt"
And user "Alice" has created the following resource link share:
| resource | testfile.txt |
| space | Personal |
| permissionsRole | view |
| password | %public% |
And user "Alice" tries to set the following password for the last link share using the Graph API:
| resource | testfile.txt |
| space | Personal |
| password | <password> |
Then the HTTP status code should be "400"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [ "error" ],
"properties": {
"error": {
"type": "object",
"required": [ "message" ],
"properties": {
"message": { "const": "<message>" }
}
}
}
}
"""
Examples:
| password | message |
| 123 | unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety |
| password | unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety |
| КуСфера | unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety |
Scenario Outline: create a public link with a password that is listed in the Banned-Password-List
Given the config "OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST" has been set to path "config/woodpecker/banned-password-list.txt"
When user "Alice" tries to create the following resource link share using the Graph API:
| space | Personal |
| resource | testfile.txt |
| permissionsRole | view |
| password | <password> |
Then the HTTP status code should be "400"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [ "error" ],
"properties": {
"error": {
"type": "object",
"required": [ "message" ],
"properties": {
"message": { "const": "<message>" }
}
}
}
}
"""
Examples:
| password | message |
| 123 | unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety |
| password | unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety |
| КуСфера | unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety |
@@ -0,0 +1,618 @@
Feature: favorites
As a user
I want to check that I can mark and unmark files and folders as favorites using the Graph API
Background:
Given user "Alice" has been created with default attributes
Scenario Outline: add a file to favorites in the personal space
Given user "Alice" has created folder "parent"
And user "Alice" has uploaded file "filesForUpload/<file>" to "/parent/<file>"
When user "Alice" marks file "parent/<file>" as favorite from space "Personal" using the Graph API
Then the HTTP status code should be "201"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"eTag",
"file",
"id",
"lastModifiedDateTime",
"name",
"parentReference",
"size"
],
"properties": {
"eTag": {
"type": "string",
"pattern": "%etag_pattern%"
},
"file": {
"type": "object",
"required": ["mimeType"],
"properties": {
"mimeType": {
"const": "<mimeType>"
}
}
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"lastModifiedDateTime": {
"type": "string",
"format": "date-time"
},
"name": {
"const": "<file>"
},
"parentReference": {
"type": "object",
"required": [
"driveId",
"driveType",
"id",
"name",
"path"
],
"properties": {
"driveId": {
"type": "string",
"pattern": "^%space_id_pattern%$"
},
"driveType": {
"const": "personal"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"name": {
"const": "parent"
},
"path": {
"const": "/parent"
}
}
},
"size": {
"type": "integer",
"const": <size>
}
}
}
"""
And as user "Alice" file "parent/<file>" should be favorited
Examples:
| file | size | mimeType |
| simple.odt | 10119 | application/vnd.oasis.opendocument.text |
| testavatar.jpg | 45343 | image/jpeg |
| simple.pdf | 17684 | application/pdf |
Scenario: add a folder to favorites in the personal space
Given user "Alice" has created folder "parent"
And user "Alice" has uploaded file with content "first" to "/parent/first.txt"
And user "Alice" has uploaded file with content "second" to "/parent/second.txt"
When user "Alice" marks folder "parent" as favorite from space "Personal" using the Graph API
Then the HTTP status code should be "201"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"eTag",
"folder",
"id",
"lastModifiedDateTime",
"name",
"parentReference",
"size"
],
"properties": {
"eTag": {
"type": "string",
"pattern": "%etag_pattern%"
},
"folder": {
"type": "object"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"lastModifiedDateTime": {
"type": "string",
"format": "date-time"
},
"name": {
"const": "parent"
},
"parentReference": {
"type": "object",
"required": [
"driveId",
"driveType",
"id",
"name",
"path"
],
"properties": {
"driveId": {
"type": "string",
"pattern": "^%space_id_pattern%$"
},
"driveType": {
"const": "personal"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"name": {
"const": "/"
},
"path": {
"const": "/"
}
}
},
"size": {
"type": "integer",
"const": 11
}
}
}
"""
And as user "Alice" folder "parent" should be favorited
Scenario: add a shared file to favorites
Given user "Brian" has been created with default attributes
And user "Alice" has uploaded file with content "КуСфера test text file" to "textfile.txt"
And user "Alice" has sent the following resource share invitation:
| resource | textfile.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile.txt" synced
When user "Brian" marks file "textfile.txt" as favorite from space "Shares" using the Graph API
Then the HTTP status code should be "201"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"eTag",
"file",
"id",
"lastModifiedDateTime",
"name",
"parentReference",
"size"
],
"properties": {
"eTag": {
"type": "string",
"pattern": "%etag_pattern%"
},
"file": {
"type": "object",
"required": ["mimeType"],
"properties": {
"mimeType": {
"const": "text/plain"
}
}
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"lastModifiedDateTime": {
"type": "string",
"format": "date-time"
},
"name": {
"const": "textfile.txt"
},
"parentReference": {
"type": "object",
"required": [
"driveId",
"driveType",
"id",
"name",
"path"
],
"properties": {
"driveId": {
"type": "string",
"pattern": "^%space_id_pattern%$"
},
"driveType": {
"const": "personal"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"name": {
"const": "/"
},
"path": {
"const": "/"
}
}
},
"size": {
"type": "integer",
"const": 24
}
}
}
"""
And as user "Brian" file "Shares/textfile.txt" should be favorited
But as user "Alice" file "textfile.txt" should not be favorited
Scenario: add a shared folder to favorites
Given user "Brian" has been created with default attributes
And user "Alice" has created folder "parent"
And user "Alice" has created folder "parent/sub"
And user "Alice" has uploaded file with content "КуСфера test text file" to "parent/textfile.txt"
And user "Alice" has sent the following resource share invitation:
| resource | parent |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "parent" synced
When user "Brian" marks folder "parent/sub" as favorite from space "Shares" using the Graph API
Then the HTTP status code should be "201"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"eTag",
"folder",
"id",
"lastModifiedDateTime",
"name",
"parentReference",
"size"
],
"properties": {
"eTag": {
"type": "string",
"pattern": "%etag_pattern%"
},
"folder": {
"type": "object"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"lastModifiedDateTime": {
"type": "string",
"format": "date-time"
},
"name": {
"const": "sub"
},
"parentReference": {
"type": "object",
"required": [
"driveId",
"driveType",
"id",
"name",
"path"
],
"properties": {
"driveId": {
"type": "string",
"pattern": "^%space_id_pattern%$"
},
"driveType": {
"const": "personal"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"name": {
"const": "parent"
},
"path": {
"const": "/parent"
}
}
},
"size": {
"type": "integer",
"const": 0
}
}
}
"""
And as user "Brian" folder "Shares/parent/sub" should be favorited
But as user "Alice" folder "parent/sub" should not be favorited
Scenario: add a file of the project space to favorites
Given user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And using spaces DAV path
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has uploaded a file inside space "new-space" with content "hello world" to "text.txt"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Viewer |
When user "Brian" marks file "text.txt" as favorite from space "new-space" using the Graph API
Then the HTTP status code should be "201"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"eTag",
"file",
"id",
"lastModifiedDateTime",
"name",
"parentReference",
"size"
],
"properties": {
"eTag": {
"type": "string",
"pattern": "%etag_pattern%"
},
"file": {
"type": "object",
"required": ["mimeType"],
"properties": {
"mimeType": {
"const": "text/plain"
}
}
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"lastModifiedDateTime": {
"type": "string",
"format": "date-time"
},
"name": {
"const": "text.txt"
},
"parentReference": {
"type": "object",
"required": [
"driveId",
"driveType",
"id",
"name",
"path"
],
"properties": {
"driveId": {
"type": "string",
"pattern": "^%space_id_pattern%$"
},
"driveType": {
"const": "project"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"name": {
"const": "/"
},
"path": {
"const": "/"
}
}
},
"size": {
"type": "integer",
"const": 11
}
}
}
"""
Scenario: add a folder of the project space to favorites
Given user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And using spaces DAV path
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has created a folder "space-folder" in space "new-space"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Viewer |
When user "Brian" marks folder "space-folder" as favorite from space "new-space" using the Graph API
Then the HTTP status code should be "201"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"eTag",
"folder",
"id",
"lastModifiedDateTime",
"name",
"parentReference",
"size"
],
"properties": {
"eTag": {
"type": "string",
"pattern": "%etag_pattern%"
},
"folder": {
"type": "object"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"lastModifiedDateTime": {
"type": "string",
"format": "date-time"
},
"name": {
"const": "space-folder"
},
"parentReference": {
"type": "object",
"required": [
"driveId",
"driveType",
"id",
"name",
"path"
],
"properties": {
"driveId": {
"type": "string",
"pattern": "^%space_id_pattern%$"
},
"driveType": {
"const": "project"
},
"id": {
"type": "string",
"pattern": "^%file_id_pattern%$"
},
"name": {
"const": "/"
},
"path": {
"const": "/"
}
}
},
"size": {
"type": "integer",
"const": 0
}
}
}
"""
Scenario: remove file from favorites from the personal space
Given user "Alice" has created folder "parent"
And user "Alice" has uploaded file "filesForUpload/textfile.txt" to "/parent/textfile.txt"
And user "Alice" has marked file "parent/textfile.txt" as favorite from space "Personal"
When user "Alice" unmarks file "parent/textfile.txt" as favorite from space "Personal" using the Graph API
Then the HTTP status code should be "204"
And as user "Alice" file "parent/textfile.txt" should not be favorited
Scenario: remove folder from favorites from the personal space
Given user "Alice" has created folder "parent"
And user "Alice" has marked folder "parent" as favorite from space "Personal"
When user "Alice" unmarks folder "parent" as favorite from space "Personal" using the Graph API
Then the HTTP status code should be "204"
And as user "Alice" folder "parent" should not be favorited
Scenario: remove file from favorites from the shares
Given user "Brian" has been created with default attributes
And user "Alice" has created folder "parent"
And user "Alice" has uploaded file "filesForUpload/textfile.txt" to "/parent/textfile.txt"
And user "Alice" has sent the following resource share invitation:
| resource | parent/textfile.txt |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile.txt" synced
And user "Brian" has marked file "textfile.txt" as favorite from space "Shares"
When user "Brian" unmarks file "textfile.txt" as favorite from space "Shares" using the Graph API
Then the HTTP status code should be "204"
And as user "Brian" file "Shares/textfile.txt" should not be favorited
Scenario: remove folder from favorites from the shares
Given user "Brian" has been created with default attributes
And user "Alice" has created folder "parent"
And user "Alice" has created folder "parent/sub"
And user "Alice" has sent the following resource share invitation:
| resource | parent |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "parent" synced
And user "Brian" has marked folder "parent/sub" as favorite from space "Shares"
When user "Brian" unmarks folder "parent/sub" as favorite from space "Shares" using the Graph API
Then the HTTP status code should be "204"
And as user "Brian" folder "Shares/parent/sub" should not be favorited
Scenario: remove file from favorites from the project space
Given user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And using spaces DAV path
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has uploaded a file inside space "new-space" with content "hello world" to "text.txt"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Viewer |
And user "Brian" has marked file "text.txt" as favorite from space "new-space"
When user "Brian" unmarks file "text.txt" as favorite from space "new-space" using the Graph API
Then the HTTP status code should be "204"
Scenario: remove folder from favorites from the project space
Given user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And using spaces DAV path
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has created a folder "space-folder" in space "new-space"
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Viewer |
And user "Brian" has marked folder "space-folder" as favorite from space "new-space"
When user "Brian" unmarks folder "space-folder" as favorite from space "new-space" using the Graph API
Then the HTTP status code should be "204"
Scenario: add a file to favorites after unmarking it as favorite in the personal space
Given user "Alice" has uploaded file "filesForUpload/testavatar.jpg" to "/testavatar.jpg"
And user "Alice" has marked file "testavatar.jpg" as favorite from space "Personal"
And user "Alice" has unmarked file "testavatar.jpg" as favorite from space "Personal"
When user "Alice" marks file "/testavatar.jpg" as favorite from space "Personal" using the Graph API
Then the HTTP status code should be "201"
And as user "Alice" file "testavatar.jpg" should be favorited
Scenario: add a file to favorites twice
Given user "Alice" has uploaded file "filesForUpload/testavatar.jpg" to "/testavatar.jpg"
And user "Alice" has marked file "testavatar.jpg" as favorite from space "Personal"
When user "Alice" marks file "/testavatar.jpg" as favorite from space "Personal" using the Graph API
Then the HTTP status code should be "201"
And as user "Alice" file "testavatar.jpg" should be favorited

Some files were not shown because too many files have changed in this diff Show More