diff options
Diffstat (limited to 'build/integration')
64 files changed, 2205 insertions, 425 deletions
diff --git a/build/integration/composer.lock b/build/integration/composer.lock index 64678c07151..cc8427c78eb 100644 --- a/build/integration/composer.lock +++ b/build/integration/composer.lock @@ -9,23 +9,24 @@ "packages-dev": [ { "name": "behat/behat", - "version": "v3.16.0", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "18f5f44c80d1cf5711ec27019afcb26b8bc6c3b0" + "reference": "c465af8756adaaa6d962c3176a0a6c594361809b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/18f5f44c80d1cf5711ec27019afcb26b8bc6c3b0", - "reference": "18f5f44c80d1cf5711ec27019afcb26b8bc6c3b0", + "url": "https://api.github.com/repos/Behat/Behat/zipball/c465af8756adaaa6d962c3176a0a6c594361809b", + "reference": "c465af8756adaaa6d962c3176a0a6c594361809b", "shasum": "" }, "require": { - "behat/gherkin": "^4.10.0", - "behat/transliterator": "^1.5", + "behat/gherkin": "^4.12.0", "composer-runtime-api": "^2.2", + "composer/xdebug-handler": "^1.4 || ^2.0 || ^3.0", "ext-mbstring": "*", + "nikic/php-parser": "^4.19.2 || ^5.2", "php": "8.1.* || 8.2.* || 8.3.* || 8.4.* ", "psr/container": "^1.0 || ^2.0", "symfony/config": "^5.4 || ^6.4 || ^7.0", @@ -36,11 +37,13 @@ "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "herrera-io/box": "~1.6.1", + "friendsofphp/php-cs-fixer": "^3.68", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.6", + "rector/rector": "^2.0", + "sebastian/diff": "^4.0", "symfony/polyfill-php84": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.0", - "vimeo/psalm": "^4.8" + "symfony/process": "^5.4 || ^6.4 || ^7.0" }, "suggest": { "ext-dom": "Needed to output test results in JUnit format." @@ -59,7 +62,9 @@ "Behat\\Hook\\": "src/Behat/Hook/", "Behat\\Step\\": "src/Behat/Step/", "Behat\\Behat\\": "src/Behat/Behat/", - "Behat\\Testwork\\": "src/Behat/Testwork/" + "Behat\\Config\\": "src/Behat/Config/", + "Behat\\Testwork\\": "src/Behat/Testwork/", + "Behat\\Transformation\\": "src/Behat/Transformation/" } }, "notification-url": "https://packagist.org/downloads/", @@ -91,31 +96,37 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.16.0" + "source": "https://github.com/Behat/Behat/tree/v3.23.0" }, - "time": "2024-11-08T12:25:17+00:00" + "time": "2025-07-15T16:58:54+00:00" }, { "name": "behat/gherkin", - "version": "v4.10.0", + "version": "v4.14.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6" + "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6", - "reference": "cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", + "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", "shasum": "" }, "require": { - "php": "~7.2|~8.0" + "composer-runtime-api": "^2.2", + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" }, "require-dev": { - "cucumber/cucumber": "dev-gherkin-24.1.0", - "phpunit/phpunit": "~8|~9", - "symfony/yaml": "~3|~4|~5|~6|~7" + "cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1", + "friendsofphp/php-cs-fixer": "^3.65", + "mikey179/vfsstream": "^1.6", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^10.5", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -127,8 +138,8 @@ } }, "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" + "psr-4": { + "Behat\\Gherkin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -139,11 +150,11 @@ { "name": "Konstantin Kudryashov", "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "homepage": "https://everzet.com" } ], "description": "Gherkin DSL parser for PHP", - "homepage": "http://behat.org/", + "homepage": "https://behat.org/", "keywords": [ "BDD", "Behat", @@ -154,58 +165,154 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.10.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.14.0" }, - "time": "2024-10-19T14:46:06+00:00" + "time": "2025-05-23T15:06:40+00:00" }, { - "name": "behat/transliterator", - "version": "v1.5.0", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/Behat/Transliterator.git", - "reference": "baac5873bac3749887d28ab68e2f74db3a4408af" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af", - "reference": "baac5873bac3749887d28ab68e2f74db3a4408af", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { - "php": ">=7.2" + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "chuyskywalker/rolling-curl": "^3.1", - "php-yaoi/php-yaoi": "^1.0", - "phpunit/phpunit": "^8.5.25 || ^9.5.19" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "Behat\\Transliterator\\": "src/Behat/Transliterator" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Artistic-1.0" + "MIT" ], - "description": "String transliterator", + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", "keywords": [ - "i18n", - "slug", - "transliterator" + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { - "issues": "https://github.com/Behat/Transliterator/issues", - "source": "https://github.com/Behat/Transliterator/tree/v1.5.0" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" }, - "time": "2022-03-30T09:27:43+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" }, { "name": "dms/phpunit-arraysubset-asserts", @@ -323,16 +430,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { @@ -429,7 +536,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, "funding": [ { @@ -445,20 +552,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { @@ -512,7 +619,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.2.0" }, "funding": [ { @@ -528,20 +635,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { @@ -628,7 +735,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.7.1" }, "funding": [ { @@ -644,7 +751,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "myclabs/deep-copy", @@ -708,16 +815,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -760,9 +867,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "phar-io/manifest", @@ -3071,16 +3178,16 @@ }, { "name": "symfony/config", - "version": "v6.4.14", + "version": "v6.4.22", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "url": "https://api.github.com/repos/symfony/config/zipball/af5917a3b1571f54689e56677a3f06440d2fe4c7", + "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7", "shasum": "" }, "require": { @@ -3126,7 +3233,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.14" + "source": "https://github.com/symfony/config/tree/v6.4.22" }, "funding": [ { @@ -3142,20 +3249,20 @@ "type": "tidelift" } ], - "time": "2024-11-04T11:33:53+00:00" + "time": "2025-05-14T06:00:01+00:00" }, { "name": "symfony/console", - "version": "v6.4.14", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "897c2441ed4eec8a8a2c37b943427d24dba3f26b" + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/897c2441ed4eec8a8a2c37b943427d24dba3f26b", - "reference": "897c2441ed4eec8a8a2c37b943427d24dba3f26b", + "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93", + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93", "shasum": "" }, "require": { @@ -3220,7 +3327,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.14" + "source": "https://github.com/symfony/console/tree/v6.4.23" }, "funding": [ { @@ -3236,20 +3343,20 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:34:40+00:00" + "time": "2025-06-27T19:37:22+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.13", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "728ae8f4e190133ce99d6d5f0bc1e8c8bd7c7a96" + "reference": "0d9f24f3de0a83573fce5c9ed025d6306c6e166b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/728ae8f4e190133ce99d6d5f0bc1e8c8bd7c7a96", - "reference": "728ae8f4e190133ce99d6d5f0bc1e8c8bd7c7a96", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0d9f24f3de0a83573fce5c9ed025d6306c6e166b", + "reference": "0d9f24f3de0a83573fce5c9ed025d6306c6e166b", "shasum": "" }, "require": { @@ -3257,7 +3364,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -3301,7 +3408,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.13" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.23" }, "funding": [ { @@ -3317,20 +3424,20 @@ "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2025-06-23T06:49:06+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -3338,12 +3445,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3368,7 +3475,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3384,7 +3491,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher", @@ -3468,16 +3575,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -3486,12 +3593,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3524,7 +3631,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -3540,7 +3647,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", @@ -3610,7 +3717,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -3634,8 +3741,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3669,7 +3776,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -3689,7 +3796,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -3710,8 +3817,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3747,7 +3854,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -3767,7 +3874,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3788,8 +3895,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3828,7 +3935,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -3848,19 +3955,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -3872,8 +3980,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3908,7 +4016,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -3924,20 +4032,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -3950,12 +4058,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3991,7 +4099,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -4007,20 +4115,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v6.4.13", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "38371c60c71c72b3d64d8d76f6b1bb81a2cc3627" + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/38371c60c71c72b3d64d8d76f6b1bb81a2cc3627", - "reference": "38371c60c71c72b3d64d8d76f6b1bb81a2cc3627", + "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", "shasum": "" }, "require": { @@ -4077,7 +4185,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.13" + "source": "https://github.com/symfony/string/tree/v6.4.21" }, "funding": [ { @@ -4093,20 +4201,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-04-18T15:23:29+00:00" }, { "name": "symfony/translation", - "version": "v6.4.13", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "bee9bfabfa8b4045a66bf82520e492cddbaffa66" + "reference": "de8afa521e04a5220e9e58a1dc99971ab7cac643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/bee9bfabfa8b4045a66bf82520e492cddbaffa66", - "reference": "bee9bfabfa8b4045a66bf82520e492cddbaffa66", + "url": "https://api.github.com/repos/symfony/translation/zipball/de8afa521e04a5220e9e58a1dc99971ab7cac643", + "reference": "de8afa521e04a5220e9e58a1dc99971ab7cac643", "shasum": "" }, "require": { @@ -4172,7 +4280,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.13" + "source": "https://github.com/symfony/translation/tree/v6.4.23" }, "funding": [ { @@ -4188,20 +4296,20 @@ "type": "tidelift" } ], - "time": "2024-09-27T18:14:25+00:00" + "time": "2025-06-26T21:24:02+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", - "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", "shasum": "" }, "require": { @@ -4209,12 +4317,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -4250,7 +4358,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" }, "funding": [ { @@ -4266,20 +4374,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-27T08:32:26+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.13", + "version": "v6.4.22", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f605f72a363f8743001038a176eeb2a11223b51" + "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f605f72a363f8743001038a176eeb2a11223b51", - "reference": "0f605f72a363f8743001038a176eeb2a11223b51", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/f28cf841f5654955c9f88ceaf4b9dc29571988a9", + "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9", "shasum": "" }, "require": { @@ -4327,7 +4435,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.13" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.22" }, "funding": [ { @@ -4343,20 +4451,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-05-14T13:00:13+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.13", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9" + "reference": "93e29e0deb5f1b2e360adfb389a20d25eb81a27b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", - "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/93e29e0deb5f1b2e360adfb389a20d25eb81a27b", + "reference": "93e29e0deb5f1b2e360adfb389a20d25eb81a27b", "shasum": "" }, "require": { @@ -4399,7 +4507,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.13" + "source": "https://github.com/symfony/yaml/tree/v6.4.23" }, "funding": [ { @@ -4415,7 +4523,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-06-03T06:46:12+00:00" }, { "name": "theseer/tokenizer", @@ -4470,11 +4578,11 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "platform-overrides": { "php": "8.1" }, diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index 21e0cdb3309..0a3fe4fd823 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -16,7 +16,7 @@ default: - "%paths.base%/../features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -39,7 +39,7 @@ default: - "%paths.base%/../comments_features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -62,7 +62,7 @@ default: - "%paths.base%/../dav_features" contexts: - DavFeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -80,12 +80,14 @@ default: - CommandLineContext: baseUrl: http://localhost:8080 ocPath: ../../ + - PrincipalPropertySearchContext: + baseUrl: http://localhost:8080 federation: paths: - "%paths.base%/../federation_features" contexts: - FederationContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -95,7 +97,7 @@ default: - "%paths.base%/../files_features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -113,12 +115,28 @@ default: - CommandLineContext: baseUrl: http://localhost:8080 ocPath: ../../ + - MetadataContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 + files_conversion: + paths: + - "%paths.base%/../file_conversions" + contexts: + - ConversionsContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 capabilities: paths: - "%paths.base%/../capabilities_features" contexts: - CapabilitiesContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -128,7 +146,7 @@ default: - "%paths.base%/../collaboration_features" contexts: - CollaborationContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -138,7 +156,7 @@ default: - "%paths.base%/../sharees_features" contexts: - ShareesContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -148,7 +166,7 @@ default: - "%paths.base%/../sharing_features" contexts: - SharingContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -159,7 +177,7 @@ default: - "%paths.base%/../videoverification_features" contexts: - SharingContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -170,7 +188,7 @@ default: - "%paths.base%/../setup_features" contexts: - SetupContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -220,10 +238,41 @@ default: - "%paths.base%/../remoteapi_features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin regular_user_password: 123456 - RemoteContext: - remote: http://localhost:8080 + remote: http://localhost:8080 + + ratelimiting: + paths: + - "%paths.base%/../ratelimiting_features" + contexts: + - RateLimitingContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 + routing: + paths: + - "%paths.base%/../routing_features" + contexts: + - RoutingContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 + theming: + paths: + - "%paths.base%/../theming_features" + contexts: + - FeatureContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 diff --git a/build/integration/data/clouds.jpg b/build/integration/data/clouds.jpg Binary files differnew file mode 100644 index 00000000000..2433b140766 --- /dev/null +++ b/build/integration/data/clouds.jpg diff --git a/build/integration/data/clouds.jpg.license b/build/integration/data/clouds.jpg.license new file mode 100644 index 00000000000..d7c54c39d02 --- /dev/null +++ b/build/integration/data/clouds.jpg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2019 CHUTTERSNAP <https://unsplash.com/@chuttersnap> <https://unsplash.com/photos/blue-clouds-under-white-sky-9AqIdzEc9pY>" +SPDX-License-Identifier: LicenseRef-Unsplash diff --git a/build/integration/dav_features/caldav.feature b/build/integration/dav_features/caldav.feature index 031685b580d..f324f720bbd 100644 --- a/build/integration/dav_features/caldav.feature +++ b/build/integration/dav_features/caldav.feature @@ -87,3 +87,8 @@ Feature: caldav When "user0" requests principal "users/user0" on the endpoint "/remote.php/dav/principals/" Then The CalDAV response should be multi status And The CalDAV response should contain a property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL" with a href value "/remote.php/dav/calendars/user0/MyCalendar2/" + + Scenario: Should create default calendar on first login + Given user "first-login" exists + When "first-login" requests calendar "first-login/personal" on the endpoint "/remote.php/dav/calendars/" + Then The CalDAV HTTP status code should be "207" diff --git a/build/integration/dav_features/carddav.feature b/build/integration/dav_features/carddav.feature index ffee11a284f..35e85639817 100644 --- a/build/integration/dav_features/carddav.feature +++ b/build/integration/dav_features/carddav.feature @@ -49,7 +49,6 @@ Feature: carddav |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| Scenario: Exporting the picture of ones own contact Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" @@ -63,7 +62,6 @@ Feature: carddav |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| Scenario: Create addressbook request for non-existing addressbook of another user Given user "user0" exists @@ -79,3 +77,7 @@ Feature: carddav Then The CardDAV HTTP status code should be "404" And The CardDAV exception is "Sabre\DAV\Exception\NotFound" And The CardDAV error message is "File not found: admin in 'addressbooks'" + + Scenario: Should create default addressbook on first login + Given user "first-login" exists + Then "first-login" requests addressbook "first-login/contacts" with statuscode "207" on the endpoint "/remote.php/dav/addressbooks/users/" diff --git a/build/integration/dav_features/dav-v2.feature b/build/integration/dav_features/dav-v2.feature index 02d90242a05..dbd2295497f 100644 --- a/build/integration/dav_features/dav-v2.feature +++ b/build/integration/dav_features/dav-v2.feature @@ -12,6 +12,16 @@ Feature: dav-v2 When User "user0" moves file "/textfile0.txt" to "/FOLDER/textfile0.txt" Then the HTTP status code should be "201" + Scenario: Moving and overwriting it's parent + Given using new dav path + And As an "admin" + And user "user0" exists + And As an "user0" + And user "user0" created a folder "/test" + And user "user0" created a folder "/test/test" + When User "user0" moves file "/test/test" to "/test" + Then the HTTP status code should be "403" + Scenario: download a file with range using new endpoint Given using new dav path And As an "admin" @@ -31,7 +41,6 @@ Feature: dav-v2 |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| And Downloaded content should start with "Welcome to your Nextcloud account!" Scenario: Doing a GET with a web login should work without CSRF token on the new backend @@ -93,32 +102,61 @@ Feature: dav-v2 | shareType | 0 | | permissions | 31 | | shareWith | user0 | - And user "user0" accepts last share + And user "user0" accepts last share And As an "user0" When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt" Then the HTTP status code should be "201" + Scenario: Uploading a file with very long filename + Given using new dav path + And As an "admin" + And user "user0" exists + And user "user0" has a quota of "10 MB" + And As an "user0" + When User "user0" uploads file "data/textfile.txt" to "/long-filename-with-250-characters-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.txt" + Then the HTTP status code should be "201" + + Scenario: Uploading a file with a too long filename + Given using new dav path + And As an "admin" + And user "user0" exists + And user "user0" has a quota of "10 MB" + And As an "user0" + When User "user0" uploads file "data/textfile.txt" to "/long-filename-with-251-characters-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.txt" + Then the HTTP status code should be "400" + Scenario: Create a search query on image Given using new dav path And As an "admin" - And user "user0" exists - And As an "user0" - When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt" - Then Image search should work - And the response should be empty - When User "user0" uploads file "data/green-square-256.png" to "/image.png" + And user "user0" exists + And As an "user0" + When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt" + Then Image search should work + And the response should be empty + When User "user0" uploads file "data/green-square-256.png" to "/image.png" Then Image search should work - And the single response should contain a property "{DAV:}getcontenttype" with value "image/png" - - Scenario: Create a search query on favorite - Given using new dav path - And As an "admin" - And user "user0" exists - And As an "user0" - When User "user0" uploads file "data/green-square-256.png" to "/fav_image.png" - Then Favorite search should work - And the response should be empty - When user "user0" favorites element "/fav_image.png" - Then Favorite search should work - And the single response should contain a property "{http://owncloud.org/ns}favorite" with value "1" + And the single response should contain a property "{DAV:}getcontenttype" with value "image/png" + Scenario: Create a search query on favorite + Given using new dav path + And As an "admin" + And user "user0" exists + And As an "user0" + When User "user0" uploads file "data/green-square-256.png" to "/fav_image.png" + Then Favorite search should work + And the response should be empty + When user "user0" favorites element "/fav_image.png" + Then Favorite search should work + And the single response should contain a property "{http://owncloud.org/ns}favorite" with value "1" + + Scenario: Create a search query on favorite + Given using new dav path + And As an "admin" + And user "user0" exists + And As an "user0" + When User "user0" uploads file "data/green-square-256.png" to "/fav_image.png" + Then Favorite search should work + And the response should be empty + When user "user0" favorites element "/fav_image.png" + Then Favorite search should work + And the single response should contain a property "{http://owncloud.org/ns}favorite" with value "1" diff --git a/build/integration/dav_features/principal-property-search.feature b/build/integration/dav_features/principal-property-search.feature new file mode 100644 index 00000000000..b2195489263 --- /dev/null +++ b/build/integration/dav_features/principal-property-search.feature @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: principal-property-search + Background: + Given user "user0" exists + Given As an "admin" + Given invoking occ with "app:enable --force testing" + + Scenario: Find a principal by a given displayname + When searching for a principal matching "user0" + Then The search HTTP status code should be "207" + And The search response should contain "<d:href>/remote.php/dav/principals/users/user0/</d:href>" diff --git a/build/integration/dav_features/webdav-related.feature b/build/integration/dav_features/webdav-related.feature index fdf633bd580..12fd3d44c4f 100644 --- a/build/integration/dav_features/webdav-related.feature +++ b/build/integration/dav_features/webdav-related.feature @@ -38,6 +38,43 @@ Feature: webdav-related Then the HTTP status code should be "204" And Downloaded content when downloading file "/textfile0.txt" with range "bytes=0-6" should be "Welcome" + Scenario: Moving and overwriting it's parent + Given using old dav path + And As an "admin" + And user "user0" exists + And As an "user0" + And user "user0" created a folder "/test" + And user "user0" created a folder "/test/test" + When User "user0" moves file "/test/test" to "/test" + Then the HTTP status code should be "403" + + Scenario: Moving a file from shared folder to root folder + Given using old dav path + And user "user0" exists + And user "user1" exists + And user "user0" created a folder "/testshare" + And User "user0" copies file "/welcome.txt" to "/testshare/welcome.txt" + And as "user0" creating a share with + | path | testshare | + | shareType | 0 | + | shareWith | user1 | + When User "user1" moves file "/testshare/welcome.txt" to "/movedwelcome.txt" + Then As an "user1" + And Downloaded content when downloading file "/movedwelcome.txt" with range "bytes=0-6" should be "Welcome" + + Scenario: Moving a file from root folder to shared folder + Given using old dav path + And user "user0" exists + And user "user1" exists + And user "user0" created a folder "/testshare" + And as "user0" creating a share with + | path | testshare | + | shareType | 0 | + | shareWith | user1 | + When User "user1" moves file "/welcome.txt" to "/testshare/movedwelcome.txt" + Then As an "user1" + And Downloaded content when downloading file "/testshare/movedwelcome.txt" with range "bytes=0-6" should be "Welcome" + Scenario: Moving a file to a folder with no permissions Given using old dav path And As an "admin" @@ -254,7 +291,6 @@ Feature: webdav-related |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| And Downloaded content should start with "Welcome to your Nextcloud account!" Scenario: Doing a GET with a web login should work without CSRF token on the old backend @@ -669,7 +705,7 @@ Feature: webdav-related And user "user0" uploads new chunk v2 file "2" to id "chunking-random" And user "user0" uploads new chunk v2 file "4" to id "chunking-random" And user "user0" moves new chunk v2 file with id "chunking-random" - Then the upload should fail on object storage + Then the upload should fail on object storage @s3-multipart Scenario: Upload chunked file with special characters with new chunking v2 diff --git a/build/integration/features/bootstrap/Activity.php b/build/integration/features/bootstrap/Activity.php new file mode 100644 index 00000000000..4172776304d --- /dev/null +++ b/build/integration/features/bootstrap/Activity.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +trait Activity { + use BasicStructure; + + /** + * @Then last activity should be + * @param TableNode $activity + */ + public function lastActivityIs(TableNode $activity): void { + $this->sendRequestForJSON('GET', '/apps/activity/api/v2/activity'); + $this->theHTTPStatusCodeShouldBe('200'); + $data = json_decode($this->response->getBody()->getContents(), true); + $activities = $data['ocs']['data']; + /* Sort by id */ + uasort($activities, fn ($a, $b) => $a['activity_id'] <=> $b['activity_id']); + $lastActivity = array_pop($activities); + foreach ($activity->getRowsHash() as $key => $value) { + Assert::assertEquals($value, $lastActivity[$key]); + } + } +} diff --git a/build/integration/features/bootstrap/AppConfiguration.php b/build/integration/features/bootstrap/AppConfiguration.php index 5f39c58ffeb..e8580ed537b 100644 --- a/build/integration/features/bootstrap/AppConfiguration.php +++ b/build/integration/features/bootstrap/AppConfiguration.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php index ffc391c0d45..beebf1c024a 100644 --- a/build/integration/features/bootstrap/Avatar.php +++ b/build/integration/features/bootstrap/Avatar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -240,10 +241,10 @@ trait Avatar { } private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) { - if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) && - $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) && - $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) && - $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { + if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) + && $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) + && $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) + && $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { return true; } @@ -251,8 +252,8 @@ trait Avatar { } private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { - if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) && - $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { + if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) + && $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { return true; } diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index b51c0a6a34c..59a4312913e 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -8,6 +9,7 @@ use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; @@ -18,6 +20,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; @@ -120,7 +123,11 @@ trait BasicStructure { * @return string */ public function getOCSResponse($response) { - return simplexml_load_string($response->getBody())->meta[0]->statuscode; + $body = simplexml_load_string((string)$response->getBody()); + if ($body === false) { + throw new \RuntimeException('Could not parse OCS response, body is not valid XML'); + } + return $body->meta[0]->statuscode; } /** @@ -170,6 +177,8 @@ trait BasicStructure { $this->response = $client->request($verb, $fullUrl, $options); } catch (ClientException $ex) { $this->response = $ex->getResponse(); + } catch (ServerException $ex) { + $this->response = $ex->getResponse(); } } @@ -185,8 +194,8 @@ trait BasicStructure { $options = []; if ($this->currentUser === 'admin') { $options['auth'] = ['admin', 'admin']; - } elseif (strpos($this->currentUser, 'guest') !== 0) { - $options['auth'] = [$this->currentUser, self::TEST_PASSWORD]; + } elseif (strpos($this->currentUser, 'anonymous') !== 0) { + $options['auth'] = [$this->currentUser, $this->regularUser]; } if ($body instanceof TableNode) { $fd = $body->getRowsHash(); diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php index 80f8c53fc4e..459c35089fa 100644 --- a/build/integration/features/bootstrap/CalDavContext.php +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php index ae32056e17f..7d09ab6ddcf 100644 --- a/build/integration/features/bootstrap/CapabilitiesContext.php +++ b/build/integration/features/bootstrap/CapabilitiesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php index a59f0d56f96..733c98dca02 100644 --- a/build/integration/features/bootstrap/CardDavContext.php +++ b/build/integration/features/bootstrap/CardDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php index 3392f8545d9..c8abf91127e 100644 --- a/build/integration/features/bootstrap/ChecksumsContext.php +++ b/build/integration/features/bootstrap/ChecksumsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLine.php b/build/integration/features/bootstrap/CommandLine.php index 84b3dfd447f..924d723daa6 100644 --- a/build/integration/features/bootstrap/CommandLine.php +++ b/build/integration/features/bootstrap/CommandLine.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php index 5ea8d12a970..e7764356270 100644 --- a/build/integration/features/bootstrap/CommandLineContext.php +++ b/build/integration/features/bootstrap/CommandLineContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -109,19 +110,6 @@ class CommandLineContext implements \Behat\Behat\Context\Context { } /** - * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)" with received shares$/ - */ - public function transferringOwnershipPathWithIncomingShares($path, $user1, $user2) { - $path = '--path=' . $path; - if ($this->runOcc(['files:transfer-ownership', $path, $user1, $user2, '--transfer-incoming-shares=1']) === 0) { - $this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2); - } else { - // failure - $this->lastTransferPath = null; - } - } - - /** * @When /^using received transfer folder of "([^"]+)" as dav path$/ */ public function usingTransferFolderAsDavPath($user) { diff --git a/build/integration/features/bootstrap/CommentsContext.php b/build/integration/features/bootstrap/CommentsContext.php index 17795a48fb4..53001b1c204 100644 --- a/build/integration/features/bootstrap/CommentsContext.php +++ b/build/integration/features/bootstrap/CommentsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/ContactsMenu.php b/build/integration/features/bootstrap/ContactsMenu.php index 4fc3c03c5e9..f6bf6b9422b 100644 --- a/build/integration/features/bootstrap/ContactsMenu.php +++ b/build/integration/features/bootstrap/ContactsMenu.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ConversionsContext.php b/build/integration/features/bootstrap/ConversionsContext.php new file mode 100644 index 00000000000..ccd14c460f8 --- /dev/null +++ b/build/integration/features/bootstrap/ConversionsContext.php @@ -0,0 +1,60 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; +use Behat\Gherkin\Node\TableNode; + +class ConversionsContext implements Context, SnippetAcceptingContext { + use AppConfiguration; + use BasicStructure; + use WebDav; + + /** @BeforeScenario */ + public function setUpScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(true); + } + + /** @AfterScenario */ + public function tearDownScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(false); + } + + protected function resetAppConfigs() { + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileId(string $user, string $path, string $mime) { + $this->userConvertsTheSavedFileIdTo($user, $path, $mime, null); + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)" and saves it to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileIdTo(string $user, string $path, string $mime, ?string $destination) { + try { + $fileId = $this->getFileIdForPath($user, $path); + } catch (Exception $e) { + // return a fake value to keep going and be able to test the error + $fileId = 0; + } + + $data = [['fileId', $fileId], ['targetMimeType', $mime]]; + if ($destination !== null) { + $data[] = ['destination', $destination]; + } + + $this->asAn($user); + $this->sendingToWith('post', '/apps/files/api/v1/convert', new TableNode($data)); + } +} diff --git a/build/integration/features/bootstrap/DavFeatureContext.php b/build/integration/features/bootstrap/DavFeatureContext.php index acca52ccafc..ec6085cff98 100644 --- a/build/integration/features/bootstrap/DavFeatureContext.php +++ b/build/integration/features/bootstrap/DavFeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Download.php b/build/integration/features/bootstrap/Download.php index 2a66f7c3d89..549a033346e 100644 --- a/build/integration/features/bootstrap/Download.php +++ b/build/integration/features/bootstrap/Download.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ExternalStorage.php b/build/integration/features/bootstrap/ExternalStorage.php index b1e4c92810b..8fe2653a026 100644 --- a/build/integration/features/bootstrap/ExternalStorage.php +++ b/build/integration/features/bootstrap/ExternalStorage.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/FakeSMTPHelper.php b/build/integration/features/bootstrap/FakeSMTPHelper.php index caf2139faab..32387869edd 100644 --- a/build/integration/features/bootstrap/FakeSMTPHelper.php +++ b/build/integration/features/bootstrap/FakeSMTPHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php index 59f1d0068dd..ab37556f931 100644 --- a/build/integration/features/bootstrap/FeatureContext.php +++ b/build/integration/features/bootstrap/FeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -13,9 +14,16 @@ require __DIR__ . '/../../vendor/autoload.php'; * Features context. */ class FeatureContext implements Context, SnippetAcceptingContext { + use AppConfiguration; use ContactsMenu; use ExternalStorage; use Search; use WebDav; use Trashbin; + + protected function resetAppConfigs(): void { + $this->deleteServerConfig('bruteForce', 'whitelist_0'); + $this->deleteServerConfig('bruteForce', 'whitelist_1'); + $this->deleteServerConfig('bruteforcesettings', 'apply_allowlist_to_ratelimit'); + } } diff --git a/build/integration/features/bootstrap/FederationContext.php b/build/integration/features/bootstrap/FederationContext.php index bbd81396df5..95dc8119ad6 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -7,6 +8,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; @@ -168,8 +170,52 @@ class FederationContext implements Context, SnippetAcceptingContext { self::$phpFederatedServerPid = ''; } + /** + * @BeforeScenario @TrustedFederation + */ + public function theServersAreTrustingEachOther() { + $this->asAn('admin'); + // Trust the remote server on the local server + $this->usingServer('LOCAL'); + $this->sendRequestForJSON('POST', '/apps/federation/trusted-servers', ['url' => 'http://localhost:' . getenv('PORT')]); + Assert::assertTrue(($this->response->getStatusCode() === 200 || $this->response->getStatusCode() === 409)); + + // Trust the local server on the remote server + $this->usingServer('REMOTE'); + $this->sendRequestForJSON('POST', '/apps/federation/trusted-servers', ['url' => 'http://localhost:' . getenv('PORT_FED')]); + // If the server is already trusted, we expect a 409 + Assert::assertTrue(($this->response->getStatusCode() === 200 || $this->response->getStatusCode() === 409)); + } + + /** + * @AfterScenario @TrustedFederation + */ + public function theServersAreNoLongerTrustingEachOther() { + $this->asAn('admin'); + // Untrust the remote servers on the local server + $this->usingServer('LOCAL'); + $this->sendRequestForJSON('GET', '/apps/federation/trusted-servers'); + $this->theHTTPStatusCodeShouldBe('200'); + $trustedServersIDs = array_map(fn ($server) => $server->id, json_decode($this->response->getBody())->ocs->data); + foreach ($trustedServersIDs as $id) { + $this->sendRequestForJSON('DELETE', '/apps/federation/trusted-servers/' . $id); + $this->theHTTPStatusCodeShouldBe('200'); + } + + // Untrust the local server on the remote server + $this->usingServer('REMOTE'); + $this->sendRequestForJSON('GET', '/apps/federation/trusted-servers'); + $this->theHTTPStatusCodeShouldBe('200'); + $trustedServersIDs = array_map(fn ($server) => $server->id, json_decode($this->response->getBody())->ocs->data); + foreach ($trustedServersIDs as $id) { + $this->sendRequestForJSON('DELETE', '/apps/federation/trusted-servers/' . $id); + $this->theHTTPStatusCodeShouldBe('200'); + } + } + protected function resetAppConfigs() { $this->deleteServerConfig('files_sharing', 'incoming_server2server_group_share_enabled'); $this->deleteServerConfig('files_sharing', 'outgoing_server2server_group_share_enabled'); + $this->deleteServerConfig('files_sharing', 'federated_trusted_share_auto_accept'); } } diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 1b9d598645f..0c437f28a72 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -15,7 +16,7 @@ class FilesDropContext implements Context, SnippetAcceptingContext { /** * @When Dropping file :path with :content */ - public function droppingFileWith($path, $content, $nickName = null) { + public function droppingFileWith($path, $content, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -28,11 +29,11 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$path"); $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest' + 'X-REQUESTED-WITH' => 'XMLHttpRequest', ]; - if ($nickName) { - $options['headers']['X-NC-NICKNAME'] = $nickName; + if ($nickname) { + $options['headers']['X-NC-NICKNAME'] = $nickname; } $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); @@ -43,20 +44,20 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $this->response = $e->getResponse(); } } - - + + /** * @When Dropping file :path with :content as :nickName */ - public function droppingFileWithAs($path, $content, $nickName) { - $this->droppingFileWith($path, $content, $nickName); + public function droppingFileWithAs($path, $content, $nickname) { + $this->droppingFileWith($path, $content, $nickname); } /** * @When Creating folder :folder in drop */ - public function creatingFolderInDrop($folder) { + public function creatingFolderInDrop($folder, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -69,13 +70,25 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$folder"); $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest' + 'X-REQUESTED-WITH' => 'XMLHttpRequest', ]; + if ($nickname) { + $options['headers']['X-NC-NICKNAME'] = $nickname; + } + try { $this->response = $client->request('MKCOL', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $e) { $this->response = $e->getResponse(); } } + + + /** + * @When Creating folder :folder in drop as :nickName + */ + public function creatingFolderInDropWithNickname($folder, $nickname) { + return $this->creatingFolderInDrop($folder, $nickname); + } } diff --git a/build/integration/features/bootstrap/LDAPContext.php b/build/integration/features/bootstrap/LDAPContext.php index f0181b36c71..986dced77a1 100644 --- a/build/integration/features/bootstrap/LDAPContext.php +++ b/build/integration/features/bootstrap/LDAPContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php index d28c24730ba..d48ed6399c5 100644 --- a/build/integration/features/bootstrap/Mail.php +++ b/build/integration/features/bootstrap/Mail.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/MetadataContext.php b/build/integration/features/bootstrap/MetadataContext.php new file mode 100644 index 00000000000..32042590c86 --- /dev/null +++ b/build/integration/features/bootstrap/MetadataContext.php @@ -0,0 +1,124 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Behat\Context\Context; +use Behat\Step\Then; +use Behat\Step\When; +use PHPUnit\Framework\Assert; +use Sabre\DAV\Client as SClient; + +require __DIR__ . '/../../vendor/autoload.php'; + +class MetadataContext implements Context { + private string $davPath = '/remote.php/dav'; + + public function __construct( + private string $baseUrl, + private array $admin, + private string $regular_user_password, + ) { + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + #[When('User :user sets the :metadataKey prop with value :metadataValue on :fileName')] + public function userSetsProp(string $user, string $metadataKey, string $metadataValue, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:set> + <d:prop> + <nc:' . $metadataKey . '>' . $metadataValue . '</nc:' . $metadataKey . '> + </d:prop> + </d:set> +</d:propertyupdate>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body); + } + + #[When('User :user deletes the :metadataKey prop on :fileName')] + public function userDeletesProp(string $user, string $metadataKey, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:remove> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> + </d:remove> +</d:propertyupdate>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body); + } + + #[Then('User :user should see the prop :metadataKey equal to :metadataValue for file :fileName')] + public function checkPropForFile(string $user, string $metadataKey, string $metadataValue, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> +</d:propfind>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body); + $parsedResponse = $client->parseMultistatus($response['body']); + + Assert::assertEquals($parsedResponse[$davUrl]['200']['{http://nextcloud.com/ns}' . $metadataKey], $metadataValue); + } + + #[Then('User :user should not see the prop :metadataKey for file :fileName')] + public function checkPropDoesNotExistsForFile(string $user, string $metadataKey, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> +</d:propfind>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body); + $parsedResponse = $client->parseMultistatus($response['body']); + + Assert::assertEquals($parsedResponse[$davUrl]['404']['{http://nextcloud.com/ns}' . $metadataKey], null); + } + + private function getDavUrl(string $user, string $fileName) { + return $this->davPath . '/files/' . $user . $fileName; + } +} diff --git a/build/integration/features/bootstrap/PrincipalPropertySearchContext.php b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php new file mode 100644 index 00000000000..9dfd9379240 --- /dev/null +++ b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php @@ -0,0 +1,141 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use GuzzleHttp\BodySummarizer; +use GuzzleHttp\Client; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Utils; +use Psr\Http\Message\ResponseInterface; + +class PrincipalPropertySearchContext implements Context { + private string $baseUrl; + private Client $client; + private ResponseInterface $response; + + public function __construct(string $baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function setUpScenario(): void { + $this->client = $this->createGuzzleInstance(); + } + + /** + * Create a Guzzle client with a higher truncateAt value to read full error responses. + */ + private function createGuzzleInstance(): Client { + $bodySummarizer = new BodySummarizer(2048); + + $stack = new HandlerStack(Utils::chooseHandler()); + $stack->push(Middleware::httpErrors($bodySummarizer), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return new Client(['handler' => $stack]); + } + + /** + * @When searching for a principal matching :match + * @param string $match + * @throws \Exception + */ + public function principalPropertySearch(string $match) { + $davUrl = $this->baseUrl . '/remote.php/dav/'; + $user = 'admin'; + $password = 'admin'; + + $this->response = $this->client->request( + 'REPORT', + $davUrl, + [ + 'body' => '<x0:principal-property-search xmlns:x0="DAV:" test="anyof"> + <x0:property-search> + <x0:prop> + <x0:displayname/> + <x2:email-address xmlns:x2="http://sabredav.org/ns"/> + </x0:prop> + <x0:match>' . $match . '</x0:match> + </x0:property-search> + <x0:prop> + <x0:displayname/> + <x1:calendar-user-type xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:calendar-user-address-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x0:principal-URL/> + <x0:alternate-URI-set/> + <x2:email-address xmlns:x2="http://sabredav.org/ns"/> + <x3:language xmlns:x3="http://nextcloud.com/ns"/> + <x1:calendar-home-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-inbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-outbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-default-calendar-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x3:resource-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-make xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-model xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-is-electric xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-range xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-seating-capacity xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-contact-person xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-contact-person-vcard xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-seating-capacity xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-address xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-story xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-room-number xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-features xmlns:x3="http://nextcloud.com/ns"/> + </x0:prop> + <x0:apply-to-principal-collection-set/> +</x0:principal-property-search> +', + 'auth' => [ + $user, + $password, + ], + 'headers' => [ + 'Content-Type' => 'application/xml; charset=UTF-8', + 'Depth' => '0', + ], + ] + ); + } + + /** + * @Then The search HTTP status code should be :code + * @param string $code + * @throws \Exception + */ + public function theHttpStatusCodeShouldBe(string $code): void { + if ((int)$code !== $this->response->getStatusCode()) { + throw new \Exception('Expected ' . (int)$code . ' got ' . $this->response->getStatusCode()); + } + } + + /** + * @Then The search response should contain :needle + * @param string $needle + * @throws \Exception + */ + public function theResponseShouldContain(string $needle): void { + $body = $this->response->getBody()->getContents(); + + if (str_contains($body, $needle) === false) { + throw new \Exception('Response does not contain "' . $needle . '"'); + } + } +} diff --git a/build/integration/features/bootstrap/Provisioning.php b/build/integration/features/bootstrap/Provisioning.php index 9e3e8ccfd8b..935ad2a4a1d 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -1,9 +1,12 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ + +use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Message\ResponseInterface; use PHPUnit\Framework\Assert; @@ -124,7 +127,7 @@ trait Provisioning { * @Then /^user "([^"]*)" has$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ public function userHasSetting($user, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; @@ -145,12 +148,43 @@ trait Provisioning { if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) { $expectedValues = explode(';', $setting[1]); foreach ($expectedValues as $expected) { - Assert::assertTrue(in_array($expected, $value['element'], true)); + Assert::assertTrue(in_array($expected, $value['element'], true), 'Data wrong for field: ' . $setting[0]); } } elseif (isset($value[0])) { - Assert::assertEqualsCanonicalizing($setting[1], $value[0]); + Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]); } else { - Assert::assertEquals('', $setting[1]); + Assert::assertEquals('', $setting[1], 'Data wrong for field: ' . $setting[0]); + } + } + } + + /** + * @Then /^user "([^"]*)" has the following profile data$/ + */ + public function userHasProfileData(string $user, ?TableNode $settings): void { + $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/profile/$user"; + $client = new Client(); + $options = []; + if ($this->currentUser === 'admin') { + $options['auth'] = $this->adminUser; + } else { + $options['auth'] = [$this->currentUser, $this->regularUser]; + } + $options['headers'] = [ + 'OCS-APIREQUEST' => 'true', + 'Accept' => 'application/json', + ]; + + $response = $client->get($fullUrl, $options); + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + $data = $data['ocs']['data']; + foreach ($settings->getRows() as $setting) { + Assert::assertArrayHasKey($setting[0], $data, 'Profile data field missing: ' . $setting[0]); + if ($setting[1] === 'NULL') { + Assert::assertNull($data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]); + } else { + Assert::assertEquals($setting[1], $data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]); } } } @@ -159,7 +193,7 @@ trait Provisioning { * @Then /^group "([^"]*)" has$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ public function groupHasSetting($group, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group"; @@ -191,7 +225,7 @@ trait Provisioning { * @Then /^user "([^"]*)" has editable fields$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $fields + * @param TableNode|null $fields */ public function userHasEditableFields($user, $fields) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields"; @@ -221,9 +255,9 @@ trait Provisioning { * @Then /^search users by phone for region "([^"]*)" with$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ - public function searchUserByPhone($region, \Behat\Gherkin\Node\TableNode $searchTable) { + public function searchUserByPhone($region, TableNode $searchTable) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone"; $client = new Client(); $options = []; @@ -624,10 +658,10 @@ trait Provisioning { /** * @Then /^users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function theUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRows(); $usersSimplified = $this->simplifyArray($users); $respondedArray = $this->getArrayOfUsersResponded($this->response); @@ -637,10 +671,10 @@ trait Provisioning { /** * @Then /^phone matches returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function thePhoneUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRowsHash(); $listCheckedElements = simplexml_load_string($this->response->getBody())->data; $respondedArray = json_decode(json_encode($listCheckedElements), true); @@ -650,10 +684,10 @@ trait Provisioning { /** * @Then /^detailed users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function theDetailedUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRows(); $usersSimplified = $this->simplifyArray($users); $respondedArray = $this->getArrayOfDetailedUsersResponded($this->response); @@ -664,10 +698,10 @@ trait Provisioning { /** * @Then /^groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theGroupsShouldBe($groupsList) { - if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { + if ($groupsList instanceof TableNode) { $groups = $groupsList->getRows(); $groupsSimplified = $this->simplifyArray($groups); $respondedArray = $this->getArrayOfGroupsResponded($this->response); @@ -677,10 +711,10 @@ trait Provisioning { /** * @Then /^subadmin groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theSubadminGroupsShouldBe($groupsList) { - if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { + if ($groupsList instanceof TableNode) { $groups = $groupsList->getRows(); $groupsSimplified = $this->simplifyArray($groups); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); @@ -690,10 +724,10 @@ trait Provisioning { /** * @Then /^apps returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $appList + * @param TableNode|null $appList */ public function theAppsShouldBe($appList) { - if ($appList instanceof \Behat\Gherkin\Node\TableNode) { + if ($appList instanceof TableNode) { $apps = $appList->getRows(); $appsSimplified = $this->simplifyArray($apps); $respondedArray = $this->getArrayOfAppsResponded($this->response); @@ -703,7 +737,7 @@ trait Provisioning { /** * @Then /^subadmin users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theSubadminUsersShouldBe($groupsList) { $this->theSubadminGroupsShouldBe($groupsList); @@ -882,7 +916,7 @@ trait Provisioning { * @param string $quota */ public function userHasAQuotaOf($user, $quota) { - $body = new \Behat\Gherkin\Node\TableNode([ + $body = new TableNode([ 0 => ['key', 'quota'], 1 => ['value', $quota], ]); @@ -950,7 +984,7 @@ trait Provisioning { /** * @Then /^user "([^"]*)" has not$/ */ - public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) { + public function userHasNotSetting($user, TableNode $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; $client = new Client(); $options = []; diff --git a/build/integration/features/bootstrap/RateLimitingContext.php b/build/integration/features/bootstrap/RateLimitingContext.php new file mode 100644 index 00000000000..15c8c5c8379 --- /dev/null +++ b/build/integration/features/bootstrap/RateLimitingContext.php @@ -0,0 +1,31 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; + +class RateLimitingContext implements Context { + use BasicStructure; + use CommandLine; + use Provisioning; + + /** + * @BeforeScenario @RateLimiting + */ + public function enableRateLimiting() { + // Enable rate limiting for the tests. + // Ratelimiting is disabled by default, so we need to enable it + $this->runOcc(['config:system:set', 'ratelimit.protection.enabled', '--value', 'true', '--type', 'bool']); + } + + /** + * @AfterScenario @RateLimiting + */ + public function disableRateLimiting() { + // Restore the default rate limiting configuration. + // Ratelimiting is disabled by default, so we need to disable it + $this->runOcc(['config:system:set', 'ratelimit.protection.enabled', '--value', 'false', '--type', 'bool']); + } +} diff --git a/build/integration/features/bootstrap/RemoteContext.php b/build/integration/features/bootstrap/RemoteContext.php index ae9da4b3614..6102f686ea7 100644 --- a/build/integration/features/bootstrap/RemoteContext.php +++ b/build/integration/features/bootstrap/RemoteContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/RoutingContext.php b/build/integration/features/bootstrap/RoutingContext.php new file mode 100644 index 00000000000..762570547e0 --- /dev/null +++ b/build/integration/features/bootstrap/RoutingContext.php @@ -0,0 +1,19 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; + +require __DIR__ . '/../../vendor/autoload.php'; + +class RoutingContext implements Context, SnippetAcceptingContext { + use Provisioning; + use AppConfiguration; + use CommandLine; + + protected function resetAppConfigs(): void { + } +} diff --git a/build/integration/features/bootstrap/Search.php b/build/integration/features/bootstrap/Search.php index 47259be769c..49a4fe92822 100644 --- a/build/integration/features/bootstrap/Search.php +++ b/build/integration/features/bootstrap/Search.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php index 96cb00d8601..aa131cec597 100644 --- a/build/integration/features/bootstrap/SetupContext.php +++ b/build/integration/features/bootstrap/SetupContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php index e152a749bfa..37e0e63e547 100644 --- a/build/integration/features/bootstrap/ShareesContext.php +++ b/build/integration/features/bootstrap/ShareesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index a58a1b4fda3..0cc490ff110 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -54,8 +55,12 @@ trait Sharing { $fd = $body->getRowsHash(); if (array_key_exists('expireDate', $fd)) { $dateModification = $fd['expireDate']; - if (!empty($dateModification)) { + if ($dateModification === 'null') { + $fd['expireDate'] = null; + } elseif (!empty($dateModification)) { $fd['expireDate'] = date('Y-m-d', strtotime($dateModification)); + } else { + $fd['expireDate'] = ''; } } $options['form_params'] = $fd; @@ -332,6 +337,8 @@ trait Sharing { return $this->isExpectedUrl((string)$data->$field, 'index.php/s/'); } elseif ($contentExpected == $data->$field) { return true; + } else { + print($data->$field); } return false; } @@ -557,18 +564,18 @@ trait Sharing { ]; $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash()); - if (!array_key_exists('uid_file_owner', $expectedFields) && - array_key_exists('uid_owner', $expectedFields)) { + if (!array_key_exists('uid_file_owner', $expectedFields) + && array_key_exists('uid_owner', $expectedFields)) { $expectedFields['uid_file_owner'] = $expectedFields['uid_owner']; } - if (!array_key_exists('displayname_file_owner', $expectedFields) && - array_key_exists('displayname_owner', $expectedFields)) { + if (!array_key_exists('displayname_file_owner', $expectedFields) + && array_key_exists('displayname_owner', $expectedFields)) { $expectedFields['displayname_file_owner'] = $expectedFields['displayname_owner']; } - if (array_key_exists('share_type', $expectedFields) && - $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */ && - array_key_exists('share_with', $expectedFields)) { + if (array_key_exists('share_type', $expectedFields) + && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */ + && array_key_exists('share_with', $expectedFields)) { if ($expectedFields['share_with'] === 'private_conversation') { $expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/'; } else { diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php index 97c2a35ad84..a9dd99108a9 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -17,6 +18,7 @@ class SharingContext implements Context, SnippetAcceptingContext { use Trashbin; use AppConfiguration; use CommandLine; + use Activity; protected function resetAppConfigs() { $this->deleteServerConfig('core', 'shareapi_default_permissions'); @@ -27,6 +29,9 @@ class SharingContext implements Context, SnippetAcceptingContext { $this->deleteServerConfig('core', 'shareapi_default_expire_date'); $this->deleteServerConfig('core', 'shareapi_expire_after_n_days'); $this->deleteServerConfig('core', 'link_defaultExpDays'); + $this->deleteServerConfig('core', 'shareapi_allow_federation_on_public_shares'); + $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); + $this->deleteServerConfig('core', 'shareapi_allow_view_without_download'); $this->runOcc(['config:system:delete', 'share_folder']); } diff --git a/build/integration/features/bootstrap/TagsContext.php b/build/integration/features/bootstrap/TagsContext.php index 262271e3710..c64626de68d 100644 --- a/build/integration/features/bootstrap/TagsContext.php +++ b/build/integration/features/bootstrap/TagsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -255,9 +256,9 @@ class TagsContext implements \Behat\Behat\Context\Context { foreach ($table->getRowsHash() as $rowDisplayName => $row) { foreach ($tags as $key => $tag) { if ( - $tag['display-name'] === $rowDisplayName && - $tag['user-visible'] === $row[0] && - $tag['user-assignable'] === $row[1] + $tag['display-name'] === $rowDisplayName + && $tag['user-visible'] === $row[0] + && $tag['user-assignable'] === $row[1] ) { unset($tags[$key]); } diff --git a/build/integration/features/bootstrap/TalkContext.php b/build/integration/features/bootstrap/TalkContext.php index fe248e1af7c..6f351c30ccf 100644 --- a/build/integration/features/bootstrap/TalkContext.php +++ b/build/integration/features/bootstrap/TalkContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Theming.php b/build/integration/features/bootstrap/Theming.php new file mode 100644 index 00000000000..f44a6533a1b --- /dev/null +++ b/build/integration/features/bootstrap/Theming.php @@ -0,0 +1,49 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +trait Theming { + + private bool $undoAllThemingChangesAfterScenario = false; + + /** + * @AfterScenario + */ + public function undoAllThemingChanges() { + if (!$this->undoAllThemingChangesAfterScenario) { + return; + } + + $this->loggingInUsingWebAs('admin'); + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/undoAllChanges'); + + $this->undoAllThemingChangesAfterScenario = false; + } + + /** + * @When logged in admin uploads theming image for :key from file :source + * + * @param string $key + * @param string $source + */ + public function loggedInAdminUploadsThemingImageForFromFile(string $key, string $source) { + $this->undoAllThemingChangesAfterScenario = true; + + $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/uploadImage?key=' . $key, + [ + 'multipart' => [ + [ + 'name' => 'image', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } +} diff --git a/build/integration/features/bootstrap/Trashbin.php b/build/integration/features/bootstrap/Trashbin.php index 6c8fd5e4fb6..dfcc23289a7 100644 --- a/build/integration/features/bootstrap/Trashbin.php +++ b/build/integration/features/bootstrap/Trashbin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index e71502b6b0c..2cb37002ac0 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -262,7 +263,11 @@ trait WebDav { 'Accept' => 'application/zip' ]; - $this->response = $client->request('GET', $fullUrl, $options); + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } } /** diff --git a/build/integration/features/contacts-menu.feature b/build/integration/features/contacts-menu.feature index f01b34aa1ba..772c0e5405c 100644 --- a/build/integration/features/contacts-menu.feature +++ b/build/integration/features/contacts-menu.feature @@ -71,8 +71,6 @@ Feature: contacts-menu And searched contact "1" is named "Test name" And searched contact "2" is named "user2" - - Scenario: users can not be found by display name if visibility is private Given user "user0" exists And user "user1" exists @@ -80,11 +78,11 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | displayname | Test name | - | displaynameScope | private | + | displaynameScope | v2-private | And Logging in using web as "user2" And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken | displayname | Another test name | - | displaynameScope | contacts | + | displaynameScope | v2-federated | When Logging in using web as "user0" And searching for contacts matching with "test" # Disabled because it regularly fails on drone: @@ -98,11 +96,11 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | email | test@example.com | - | emailScope | private | + | emailScope | v2-private | And Logging in using web as "user2" And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken | email | another_test@example.com | - | emailScope | contacts | + | emailScope | v2-federated | # Disabled because it regularly fails on drone: # When Logging in using web as "user0" # And searching for contacts matching with "test" @@ -116,15 +114,15 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | displayname | Test name | - | displaynameScope | contacts | + | displaynameScope | v2-federated | | email | test@example.com | - | emailScope | private | + | emailScope | v2-private | And Logging in using web as "user2" And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken | displayname | Another test name | - | displaynameScope | private | + | displaynameScope | v2-private | | email | another_test@example.com | - | emailScope | contacts | + | emailScope | v2-federated | When Logging in using web as "user0" And searching for contacts matching with "test" Then the list of searched contacts has "2" contacts @@ -140,9 +138,9 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | displayname | Test name | - | displaynameScope | private | + | displaynameScope | v2-private | And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | displaynameScope | contacts | + | displaynameScope | v2-federated | When Logging in using web as "user0" And searching for contacts matching with "test" Then the list of searched contacts has "1" contacts @@ -154,9 +152,9 @@ Feature: contacts-menu And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken | email | test@example.com | - | emailScope | private | + | emailScope | v2-private | And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | emailScope | contacts | + | emailScope | v2-federated | # Disabled because it regularly fails on drone: # When Logging in using web as "user0" # And searching for contacts matching with "test" @@ -170,7 +168,7 @@ Feature: contacts-menu And user "user1" exists And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | displaynameScope | private | + | displaynameScope | v2-private | And As an "admin" And sending "PUT" to "/cloud/users/user1" with | key | displayname | @@ -185,7 +183,7 @@ Feature: contacts-menu And user "user1" exists And Logging in using web as "user1" And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken - | emailScope | private | + | emailScope | v2-private | And As an "admin" And sending "PUT" to "/cloud/users/user1" with | key | email | diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 12498bedd7f..8fcfb076497 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -4,6 +4,9 @@ Feature: provisioning Background: Given using api version "1" + Given parameter "whitelist_0" of app "bruteForce" is set to "127.0.0.1" + Given parameter "whitelist_1" of app "bruteForce" is set to "::1" + Given parameter "apply_allowlist_to_ratelimit" of app "bruteforcesettings" is set to "true" Scenario: Getting an not existing user Given As an "admin" @@ -69,7 +72,8 @@ Feature: provisioning | phone | | address | | website | - | twitter | + | twitter | + | bluesky | | fediverse | | organisation | | role | @@ -86,6 +90,7 @@ Feature: provisioning | address | | website | | twitter | + | bluesky | | fediverse | | organisation | | role | @@ -101,6 +106,7 @@ Feature: provisioning | address | | website | | twitter | + | bluesky | | fediverse | | organisation | | role | @@ -155,6 +161,9 @@ Feature: provisioning And sending "PUT" to "/cloud/users/brand-new-user" with | key | twitter | | value | Nextcloud | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | bluesky | + | value | nextcloud.bsky.social | And the OCS status code should be "100" And the HTTP status code should be "200" Then user "brand-new-user" has @@ -165,7 +174,37 @@ Feature: provisioning | phone | +4971125242890 | | address | Foo Bar Town | | website | https://nextcloud.com | - | twitter | Nextcloud | + | twitter | Nextcloud | + | bluesky | nextcloud.bsky.social | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | organisation | + | value | Nextcloud GmbH | + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | role | + | value | Engineer | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has the following profile data + | userId | brand-new-user | + | displayname | Brand New User | + | organisation | Nextcloud GmbH | + | role | Engineer | + | address | Foo Bar Town | + | timezone | UTC | + | timezoneOffset | 0 | + | pronouns | NULL | + + Scenario: Edit a user with mixed case emails + Given As an "admin" + And user "brand-new-user" exists + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | email | + | value | mixed-CASE@Nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has + | id | brand-new-user | + | email | mixed-case@nextcloud.com | Scenario: Edit a user account properties scopes Given user "brand-new-user" exists @@ -180,6 +219,11 @@ Feature: provisioning | value | v2-local | Then the OCS status code should be "100" And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | blueskyScope | + | value | v2-local | + Then the OCS status code should be "100" + And the HTTP status code should be "200" When sending "PUT" to "/cloud/users/brand-new-user" with | key | addressScope | | value | v2-federated | @@ -190,21 +234,6 @@ Feature: provisioning | value | v2-published | Then the OCS status code should be "100" And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | websiteScope | - | value | public | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | displaynameScope | - | value | contacts | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - When sending "PUT" to "/cloud/users/brand-new-user" with - | key | avatarScope | - | value | private | - Then the OCS status code should be "100" - And the HTTP status code should be "200" And sending "PUT" to "/cloud/users/brand-new-user" with | key | email | | value | no-reply@nextcloud.com | @@ -230,12 +259,10 @@ Feature: provisioning Then user "brand-new-user" has | id | brand-new-user | | phoneScope | v2-private | - | twitterScope | v2-local | + | twitterScope | v2-local | + | blueskyScope | v2-local | | addressScope | v2-federated | | emailScope | v2-published | - | websiteScope | v2-published | - | displaynameScope | v2-federated | - | avatarScope | v2-local | Scenario: Edit a user account multivalue property scopes Given user "brand-new-user" exists @@ -450,6 +477,7 @@ Feature: provisioning Then groups returned are | España | | admin | + | hidden_group | | new-group | Scenario: create a subadmin @@ -604,6 +632,7 @@ Feature: provisioning | settings | | sharebymail | | systemtags | + | testing | | theming | | twofactor_backupcodes | | updatenotification | @@ -629,6 +658,7 @@ Feature: provisioning And the HTTP status code should be "200" Scenario: enable an app + Given invoking occ with "app:disable testing" Given As an "admin" And app "testing" is disabled When sending "POST" to "/cloud/apps/testing" @@ -643,12 +673,14 @@ Feature: provisioning And the HTTP status code should be "200" Scenario: disable an app + Given invoking occ with "app:enable testing" Given As an "admin" And app "testing" is enabled When sending "DELETE" to "/cloud/apps/testing" Then the OCS status code should be "100" And the HTTP status code should be "200" And app "testing" is disabled + Given invoking occ with "app:enable testing" Scenario: disable an user Given As an "admin" diff --git a/build/integration/federation_features/federated.feature b/build/integration/federation_features/federated.feature index 81a3d3abd02..d3a414cb804 100644 --- a/build/integration/federation_features/federated.feature +++ b/build/integration/federation_features/federated.feature @@ -8,7 +8,7 @@ Feature: federated Scenario: Federate share a file with another server Given Using server "REMOTE" And user "user1" exists - And Using server "LOCAL" + Given Using server "LOCAL" And user "user0" exists When User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE" Then the OCS status code should be "100" @@ -30,6 +30,12 @@ Feature: federated | displayname_owner | user0 | | share_with | user1@REMOTE | | share_with_displayname | user1 | + Given Using server "REMOTE" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + Then the list of returned shares has 1 shares Scenario: Federated group share a file with another server Given Using server "REMOTE" @@ -40,7 +46,7 @@ Feature: federated And As an "admin" And Add user "gs-user1" to the group "group1" And Add user "gs-user2" to the group "group1" - And Using server "LOCAL" + Given Using server "LOCAL" And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes" And user "gs-user0" exists When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "group1" from server "REMOTE" @@ -64,11 +70,10 @@ Feature: federated | share_with | group1@REMOTE | | share_with_displayname | group1@REMOTE | - Scenario: Federate share a file with local server Given Using server "LOCAL" And user "user0" exists - And Using server "REMOTE" + Given Using server "REMOTE" And user "user1" exists When User "user1" from server "REMOTE" shares "/textfile0.txt" with user "user0" from server "LOCAL" Then the OCS status code should be "100" @@ -94,10 +99,10 @@ Feature: federated Scenario: Remote sharee can see the pending share Given Using server "REMOTE" And user "user1" exists - And Using server "LOCAL" + Given Using server "LOCAL" And user "user0" exists And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE" - And Using server "REMOTE" + Given Using server "REMOTE" And As an "user1" When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" Then the OCS status code should be "100" @@ -122,11 +127,11 @@ Feature: federated And As an "admin" And Add user "gs-user1" to the group "group1" And Add user "gs-user2" to the group "group1" - And Using server "LOCAL" + Given Using server "LOCAL" And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes" And user "gs-user0" exists When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "group1" from server "REMOTE" - And Using server "REMOTE" + Given Using server "REMOTE" And As an "gs-user1" When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" Then the OCS status code should be "100" @@ -159,7 +164,7 @@ Feature: federated Scenario: accept a pending remote share Given Using server "REMOTE" And user "user1" exists - And Using server "LOCAL" + Given Using server "LOCAL" And user "user0" exists And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE" When User "user1" from server "REMOTE" accepts last pending share @@ -175,7 +180,7 @@ Feature: federated And As an "admin" And Add user "gs-user1" to the group "group1" And Add user "gs-user2" to the group "group1" - And Using server "LOCAL" + Given Using server "LOCAL" And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes" And user "gs-user0" exists When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "group1" from server "REMOTE" @@ -187,45 +192,45 @@ Feature: federated Given Using server "REMOTE" And user "user1" exists And user "user2" exists - And Using server "LOCAL" + Given Using server "LOCAL" And user "user0" exists And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE" And User "user1" from server "REMOTE" accepts last pending share - And Using server "REMOTE" + Given Using server "REMOTE" And As an "user1" When creating a share with | path | /textfile0 (2).txt | | shareType | 0 | | shareWith | user2 | | permissions | 19 | - #Then the OCS status code should be "100" - #And the HTTP status code should be "200" - #And Share fields of last share match with - # | id | A_NUMBER | - # | item_type | file | - # | item_source | A_NUMBER | - # | share_type | 0 | - # | file_source | A_NUMBER | - # | path | /textfile0 (2).txt | - # | permissions | 19 | - # | stime | A_NUMBER | - # | storage | A_NUMBER | - # | mail_send | 1 | - # | uid_owner | user1 | - # | file_parent | A_NUMBER | - # | displayname_owner | user1 | - # | share_with | user2 | - # | share_with_displayname | user2 | + # Then the OCS status code should be "100" + # And the HTTP status code should be "200" + # And Share fields of last share match with + # | id | A_NUMBER | + # | item_type | file | + # | item_source | A_NUMBER | + # | share_type | 0 | + # | file_source | A_NUMBER | + # | path | /textfile0 (2).txt | + # | permissions | 19 | + # | stime | A_NUMBER | + # | storage | A_NUMBER | + # | mail_send | 1 | + # | uid_owner | user1 | + # | file_parent | A_NUMBER | + # | displayname_owner | user1 | + # | share_with | user2 | + # | share_with_displayname | user2 | Scenario: Overwrite a federated shared file as recipient Given Using server "REMOTE" And user "user1" exists And user "user2" exists - And Using server "LOCAL" + Given Using server "LOCAL" And user "user0" exists And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE" And User "user1" from server "REMOTE" accepts last pending share - And Using server "REMOTE" + Given Using server "REMOTE" And As an "user1" And User "user1" modifies text of "/textfile0.txt" with text "BLABLABLA" When User "user1" uploads file "../../data/user1/files/textfile0.txt" to "/textfile0 (2).txt" @@ -236,16 +241,16 @@ Feature: federated Given Using server "REMOTE" And user "user1" exists And user "user2" exists - And Using server "LOCAL" + Given Using server "LOCAL" And user "user0" exists And User "user0" from server "LOCAL" shares "/PARENT" with user "user1" from server "REMOTE" And User "user1" from server "REMOTE" accepts last pending share - And Using server "REMOTE" + Given Using server "REMOTE" And As an "user1" And User "user1" modifies text of "/textfile0.txt" with text "BLABLABLA" - #When User "user1" uploads file "../../data/user1/files/textfile0.txt" to "/PARENT (2)/textfile0.txt" - #And Downloading file "/PARENT (2)/textfile0.txt" with range "bytes=0-8" - #Then Downloaded content should be "BLABLABLA" + When User "user1" uploads file "../../data/user1/files/textfile0.txt" to "/PARENT (2)/textfile0.txt" + And Downloading file "/PARENT (2)/textfile0.txt" with range "bytes=0-8" + Then Downloaded content should be "BLABLABLA" Scenario: List federated share from another server not accepted yet Given Using server "LOCAL" @@ -256,7 +261,7 @@ Feature: federated # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And Using server "LOCAL" + Given Using server "LOCAL" When As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" Then the list of returned shares has 0 shares @@ -270,7 +275,7 @@ Feature: federated # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share When As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" @@ -296,7 +301,7 @@ Feature: federated # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share And remote server is stopped When As an "user0" @@ -318,7 +323,7 @@ Feature: federated # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share # Checking that the file exists caches the file entry, which causes an # exception to be thrown when getting the file info if the remote server is @@ -335,8 +340,6 @@ Feature: federated | user | user0 | | mountpoint | /remote-share.txt | - - Scenario: Delete federated share with another server Given Using server "LOCAL" And user "user0" exists @@ -349,13 +352,13 @@ Feature: federated And As an "user1" And sending "GET" to "/apps/files_sharing/api/v1/shares" And the list of returned shares has 1 shares - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share And as "user0" the file "/remote-share.txt" exists And As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" And the list of returned shares has 1 shares - And Using server "REMOTE" + Given Using server "REMOTE" When As an "user1" And Deleting last share Then the OCS status code should be "100" @@ -363,7 +366,7 @@ Feature: federated And As an "user1" And sending "GET" to "/apps/files_sharing/api/v1/shares" And the list of returned shares has 0 shares - And Using server "LOCAL" + Given Using server "LOCAL" And as "user0" the file "/remote-share.txt" does not exist And As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" @@ -381,7 +384,7 @@ Feature: federated And As an "user1" And sending "GET" to "/apps/files_sharing/api/v1/shares" And the list of returned shares has 1 shares - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share And as "user0" the file "/remote-share.txt" exists And As an "user0" @@ -394,7 +397,7 @@ Feature: federated And As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" And the list of returned shares has 0 shares - And Using server "REMOTE" + Given Using server "REMOTE" And As an "user1" And sending "GET" to "/apps/files_sharing/api/v1/shares" And the list of returned shares has 0 shares @@ -408,7 +411,7 @@ Feature: federated # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share And as "user0" the file "/remote-share.txt" exists And As an "user0" @@ -435,7 +438,7 @@ Feature: federated And As an "user1" And sending "GET" to "/apps/files_sharing/api/v1/shares" And the list of returned shares has 1 shares - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share And as "user0" the file "/remote-share.txt" exists And As an "user0" @@ -447,7 +450,7 @@ Feature: federated And As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" And the list of returned shares has 0 shares - And Using server "REMOTE" + Given Using server "REMOTE" And As an "user1" And sending "GET" to "/apps/files_sharing/api/v1/shares" And the list of returned shares has 0 shares @@ -461,7 +464,7 @@ Feature: federated # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And Using server "LOCAL" + Given Using server "LOCAL" And User "user0" from server "LOCAL" accepts last pending share And as "user0" the file "/remote-share.txt" exists And As an "user0" @@ -474,3 +477,115 @@ Feature: federated And As an "user0" And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" And the list of returned shares has 0 shares + + Scenario: Share to a non-trusted server will NOT auto accept + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "userfed2" exists + And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes" + When As an "user0" + When User "user0" from server "LOCAL" shares "/textfile0.txt" with user "userfed2" from server "REMOTE" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false" + And the list of returned shares has 1 shares + Given Using server "REMOTE" + And using new dav path + And As an "userfed2" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + And the list of returned shares has 1 shares + And as "userfed2" the file "/textfile0 (2).txt" does not exist + + Scenario: Share to a non-trusted server group will NOT auto accept + Given Using server "REMOTE" + And parameter "incoming_server2server_group_share_enabled" of app "files_sharing" is set to "yes" + And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes" + And user "gs-userfed3" exists + And user "gs-userfed4" exists + And group "groupfed2" exists + And As an "admin" + And Add user "gs-userfed3" to the group "groupfed2" + And Add user "gs-userfed4" to the group "groupfed2" + Given Using server "LOCAL" + And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes" + And user "gs-user0" exists + When As an "gs-user0" + When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "groupfed2" from server "REMOTE" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false" + And the list of returned shares has 1 shares + Given Using server "REMOTE" + And using new dav path + And As an "gs-userfed3" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + And the list of returned shares has 1 shares + And as "gs-userfed3" the file "/textfile0 (2).txt" does not exist + And As an "gs-userfed4" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + And the list of returned shares has 1 shares + And as "gs-userfed4" the file "/textfile0 (2).txt" does not exist + + @TrustedFederation + Scenario: Share to a trusted server auto accept + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "userfed1" exists + And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes" + When As an "user0" + When User "user0" from server "LOCAL" shares "/textfile0.txt" with user "userfed1" from server "REMOTE" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false" + And the list of returned shares has 1 shares + Given Using server "REMOTE" + And using new dav path + And As an "userfed1" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + And the list of returned shares has 0 shares + And as "userfed1" the file "/textfile0 (2).txt" exists + + @TrustedFederation + Scenario: Share to a trusted server group auto accept + Given Using server "REMOTE" + And parameter "incoming_server2server_group_share_enabled" of app "files_sharing" is set to "yes" + And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes" + And user "gs-userfed1" exists + And user "gs-userfed2" exists + And group "groupfed1" exists + And As an "admin" + And Add user "gs-userfed1" to the group "groupfed1" + And Add user "gs-userfed2" to the group "groupfed1" + Given Using server "LOCAL" + And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes" + And user "gs-user0" exists + When As an "gs-user0" + When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "groupfed1" from server "REMOTE" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false" + And the list of returned shares has 1 shares + Given Using server "REMOTE" + And using new dav path + And As an "gs-userfed1" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + And the list of returned shares has 0 shares + And as "gs-userfed1" the file "/textfile0 (2).txt" exists + And As an "gs-userfed2" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending" + And the list of returned shares has 0 shares + And as "gs-userfed2" the file "/textfile0 (2).txt" exists diff --git a/build/integration/file_conversions/file_conversions.feature b/build/integration/file_conversions/file_conversions.feature new file mode 100644 index 00000000000..92dc11a647a --- /dev/null +++ b/build/integration/file_conversions/file_conversions.feature @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-only + +Feature: conversions + Background: + Given using api version "2" + Given using new dav path + Given user "user0" exists + + Scenario: Converting a file works + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + Then as "user0" the file "/image.jpg" exists + When user "user0" converts file "/image.jpg" to "image/png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/image.png" exists + + Scenario: Converting a file to a given path works + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And User "user0" created a folder "/folder" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the folder "/folder" exists + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/folder/image.png" exists + Then as "user0" the file "/image.png" does not exist + + Scenario: Converting a file path with overwrite + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And user "user0" uploads file "data/green-square-256.png" to "/image.png" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/image.png" exists + When user "user0" converts file "/image.jpg" to "image/png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/image.png" exists + Then as "user0" the file "/image (2).png" exists + + Scenario: Converting a file path with overwrite to a given path + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And User "user0" created a folder "/folder" + And user "user0" uploads file "data/green-square-256.png" to "/folder/image.png" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the folder "/folder" exists + Then as "user0" the file "/folder/image.png" exists + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/folder/image.png" exists + Then as "user0" the file "/folder/image (2).png" exists + Then as "user0" the file "/image.png" does not exist + Then as "user0" the file "/image.jpg" exists + + Scenario: Converting a file which does not exist fails + When user "user0" converts file "/image.jpg" to "image/png" + Then the HTTP status code should be "404" + Then the OCS status code should be "404" + Then as "user0" the file "/image.jpg" does not exist + Then as "user0" the file "/image.png" does not exist + + Scenario: Converting a file to an invalid destination path fails + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png" + Then the HTTP status code should be "404" + Then the OCS status code should be "404" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/folder/image.png" does not exist + + Scenario: Converting a file to an invalid format fails + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + When user "user0" converts file "/image.jpg" to "image/invalid" + Then the HTTP status code should be "500" + Then the OCS status code should be "999" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/image.png" does not exist + +Scenario: Converting a file to a given path without extension fails + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And User "user0" created a folder "/folder" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the folder "/folder" exists + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image" + Then the HTTP status code should be "400" + Then the OCS status code should be "400" + Then as "user0" the file "/folder/image.png" does not exist + Then as "user0" the file "/image.png" does not exist + + @local_storage + Scenario: Converting a file bigger than 100 MiB fails + Given file "/image.jpg" of size 108003328 is created in local storage + Then as "user0" the folder "/local_storage" exists + Then as "user0" the file "/local_storage/image.jpg" exists + When user "user0" converts file "/local_storage/image.jpg" to "image/png" and saves it to "/image.png" + Then the HTTP status code should be "400" + Then the OCS status code should be "400" + Then as "user0" the file "/image.png" does not exist + + Scenario: Forbid conversion to a destination without create permission + Given user "user1" exists + # Share the folder with user1 + Given User "user0" created a folder "/folder" + Then As an "user0" + When creating a share with + | path | folder | + | shareWith | user1 | + | shareType | 0 | + | permissions | 1 | + Then the OCS status code should be "200" + And the HTTP status code should be "200" + # Create the folder, upload the image + Then As an "user1" + Given user "user1" accepts last share + Given as "user1" the folder "/folder" exists + Given user "user1" uploads file "data/clouds.jpg" to "/image.jpg" + Then as "user1" the file "/image.jpg" exists + # Try to convert the image to a folder where user1 has no create permission + When user "user1" converts file "/image.jpg" to "image/png" and saves it to "/folder/folder.png" + Then the OCS status code should be "403" + And the HTTP status code should be "403" + Then as "user1" the file "/folder/folder.png" does not exist diff --git a/build/integration/files_features/metadata.feature b/build/integration/files_features/metadata.feature new file mode 100644 index 00000000000..553a7b62306 --- /dev/null +++ b/build/integration/files_features/metadata.feature @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-only +Feature: metadata + + Scenario: Setting metadata works + Given user "user0" exists + When User "user0" uploads file with content "AAA" to "/test.txt" + And User "user0" sets the "metadata-files-live-photo" prop with value "metadata-value" on "/test.txt" + Then User "user0" should see the prop "metadata-files-live-photo" equal to "metadata-value" for file "/test.txt" + + Scenario: Deleting metadata works + Given user "user0" exists + When User "user0" uploads file with content "AAA" to "/test.txt" + And User "user0" sets the "metadata-files-live-photo" prop with value "metadata-value" on "/test.txt" + And User "user0" deletes the "metadata-files-live-photo" prop on "/test.txt" + Then User "user0" should not see the prop "metadata-files-live-photo" for file "/test.txt" diff --git a/build/integration/files_features/transfer-ownership.feature b/build/integration/files_features/transfer-ownership.feature index 34fed8b9efd..6f7a7944166 100644 --- a/build/integration/files_features/transfer-ownership.feature +++ b/build/integration/files_features/transfer-ownership.feature @@ -184,10 +184,10 @@ Feature: transfer-ownership And As an "user2" Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And using old dav path - And as "user0" the folder "/test" exists + And as "user0" the folder "/test" does not exist And using received transfer folder of "user1" as dav path - And as "user1" the folder "/test" does not exist - And As an "user0" + And as "user1" the folder "/test" exists + And As an "user1" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with @@ -210,13 +210,12 @@ Feature: transfer-ownership And user "user1" accepts last share When transferring ownership from "user0" to "user1" And the command was successful - And As an "user1" - Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And using old dav path - And as "user0" the folder "/test" exists + Then as "user0" the folder "/test" does not exist + When As an "user1" And using received transfer folder of "user1" as dav path - And as "user1" the folder "/test" does not exist - And As an "user1" + Then as "user1" the folder "/test" exists + And Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with @@ -242,10 +241,10 @@ Feature: transfer-ownership And As an "user2" Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And using old dav path - And as "user0" the folder "/test" exists + And as "user0" the folder "/test" does not exist And using received transfer folder of "user1" as dav path - And as "user1" the folder "/test" does not exist - And As an "user0" + And as "user1" the folder "/test" exists + And As an "user1" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with @@ -253,7 +252,7 @@ Feature: transfer-ownership | uid_file_owner | user3 | | share_with | group1 | - Scenario: transferring ownership does not transfer received shares + Scenario: transferring ownership transfers received shares Given user "user0" exists And user "user1" exists And user "user2" exists @@ -264,16 +263,16 @@ Feature: transfer-ownership And the command was successful And As an "user1" And using received transfer folder of "user1" as dav path - Then as "user1" the folder "/test" does not exist + Then as "user1" the folder "/test" exists And using old dav path - And as "user0" the folder "/test" exists + And as "user0" the folder "/test" does not exist And As an "user2" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with | uid_owner | user2 | | uid_file_owner | user2 | - | share_with | user0 | + | share_with | user1 | @local_storage Scenario: transferring ownership does not transfer external storage @@ -514,27 +513,7 @@ Feature: transfer-ownership And user "user2" accepts last share When transferring ownership of path "test" from "user0" to "user1" Then the command failed with exit code 1 - And the command output contains the text "Could not transfer files." - - Scenario: transferring ownership does not transfer received shares - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And User "user2" created a folder "/test" - And User "user0" created a folder "/sub" - And folder "/test" of user "user2" is shared with user "user0" with permissions 31 - And user "user0" accepts last share - And User "user0" moved folder "/test" to "/sub/test" - When transferring ownership of path "sub" from "user0" to "user1" - And the command was successful - And As an "user1" - And using received transfer folder of "user1" as dav path - Then as "user1" the folder "/sub" exists - And as "user1" the folder "/sub/test" does not exist - And using old dav path - And as "user0" the folder "/sub" does not exist - And Getting info of last share - And the OCS status code should be "404" + And the command error output contains the text "Moving a storage (user0/files/test) into another storage (user1) is not allowed" Scenario: transferring ownership transfers received shares into subdir when requested Given user "user0" exists @@ -548,7 +527,7 @@ Feature: transfer-ownership And User "user0" moved folder "/transfer-share" to "/sub/transfer-share" And folder "/do-not-transfer" of user "user2" is shared with user "user0" with permissions 31 And user "user0" accepts last share - When transferring ownership of path "sub" from "user0" to "user1" with received shares + When transferring ownership of path "sub" from "user0" to "user1" And the command was successful And As an "user1" And using received transfer folder of "user1" as dav path diff --git a/build/integration/files_features/windows_compatibility.feature b/build/integration/files_features/windows_compatibility.feature new file mode 100644 index 00000000000..feaaca1ed3a --- /dev/null +++ b/build/integration/files_features/windows_compatibility.feature @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: Windows compatible filenames + Background: + Given using api version "1" + And using new dav path + And As an "admin" + + Scenario: prevent upload files with invalid name + Given As an "admin" + And user "user0" exists + And invoking occ with "files:windows-compatible-filenames --enable" + Given User "user0" created a folder "/com1" + Then as "user0" the file "/com1" does not exist + + Scenario: renaming a folder with invalid name + Given As an "admin" + When invoking occ with "files:windows-compatible-filenames --disable" + And user "user0" exists + Given User "user0" created a folder "/aux" + When invoking occ with "files:windows-compatible-filenames --enable" + And invoking occ with "files:sanitize-filenames user0" + Then as "user0" the file "/aux" does not exist + And as "user0" the file "/aux (renamed)" exists + + Scenario: renaming a file with invalid base name + Given As an "admin" + When invoking occ with "files:windows-compatible-filenames --disable" + And user "user0" exists + When User "user0" uploads file with content "hello" to "/com0.txt" + And invoking occ with "files:windows-compatible-filenames --enable" + And invoking occ with "files:sanitize-filenames user0" + Then as "user0" the file "/com0.txt" does not exist + And as "user0" the file "/com0 (renamed).txt" exists + + Scenario: renaming a file with invalid extension + Given As an "admin" + When invoking occ with "files:windows-compatible-filenames --disable" + And user "user0" exists + When User "user0" uploads file with content "hello" to "/foo.txt." + And as "user0" the file "/foo.txt." exists + And invoking occ with "files:windows-compatible-filenames --enable" + And invoking occ with "files:sanitize-filenames user0" + Then as "user0" the file "/foo.txt." does not exist + And as "user0" the file "/foo.txt" exists + + Scenario: renaming a file with invalid character + Given As an "admin" + When invoking occ with "files:windows-compatible-filenames --disable" + And user "user0" exists + When User "user0" uploads file with content "hello" to "/2*2=4.txt" + And as "user0" the file "/2*2=4.txt" exists + And invoking occ with "files:windows-compatible-filenames --enable" + And invoking occ with "files:sanitize-filenames user0" + Then as "user0" the file "/2*2=4.txt" does not exist + And as "user0" the file "/2_2=4.txt" exists + + Scenario: renaming a file with invalid character and replacement setup + Given As an "admin" + When invoking occ with "files:windows-compatible-filenames --disable" + And user "user0" exists + When User "user0" uploads file with content "hello" to "/2*3=6.txt" + And as "user0" the file "/2*3=6.txt" exists + And invoking occ with "files:windows-compatible-filenames --enable" + And invoking occ with "files:sanitize-filenames --char-replacement + user0" + Then as "user0" the file "/2*3=6.txt" does not exist + And as "user0" the file "/2+3=6.txt" exists diff --git a/build/integration/filesdrop_features/filesdrop.feature b/build/integration/filesdrop_features/filesdrop.feature index 2c9156dea02..7618a31a1d0 100644 --- a/build/integration/filesdrop_features/filesdrop.feature +++ b/build/integration/filesdrop_features/filesdrop.feature @@ -33,7 +33,7 @@ Feature: FilesDrop And Downloading file "/drop/a (2).txt" Then Downloaded content should be "def" - Scenario: Files drop ignores directory + Scenario: Files drop forbid directory without a nickname Given user "user0" exists And As an "user0" And user "user0" created a folder "/drop" @@ -44,10 +44,9 @@ Feature: FilesDrop And Updating last share with | permissions | 4 | When Dropping file "/folder/a.txt" with "abc" - And Downloading file "/drop/a.txt" - Then Downloaded content should be "abc" + Then the HTTP status code should be "400" - Scenario: Files drop forbis MKCOL + Scenario: Files drop forbid MKCOL without a nickname Given user "user0" exists And As an "user0" And user "user0" created a folder "/drop" @@ -58,7 +57,33 @@ Feature: FilesDrop And Updating last share with | permissions | 4 | When Creating folder "folder" in drop - Then the HTTP status code should be "405" + Then the HTTP status code should be "400" + + Scenario: Files drop allows MKCOL with a nickname + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 3 | + | publicUpload | true | + And Updating last share with + | permissions | 4 | + When Creating folder "folder" in drop as "nickname" + Then the HTTP status code should be "201" + + Scenario: Files drop forbid subfolder creation without a nickname + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 3 | + | publicUpload | true | + And Updating last share with + | permissions | 4 | + When dropping file "/folder/a.txt" with "abc" + Then the HTTP status code should be "400" Scenario: Files request drop Given user "user0" exists @@ -71,9 +96,50 @@ Feature: FilesDrop | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | | shareWith | | When Dropping file "/folder/a.txt" with "abc" as "Alice" - And Downloading file "/drop/Alice/a.txt" + And Downloading file "/drop/Alice/folder/a.txt" + Then Downloaded content should be "abc" + + Scenario: File drop uploading folder with name of file + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder" with "its a file" as "Alice" + Then the HTTP status code should be "201" + When Dropping file "/folder/a.txt" with "abc" as "Alice" + Then the HTTP status code should be "201" + When Downloading file "/drop/Alice/folder" + Then the HTTP status code should be "200" + And Downloaded content should be "its a file" + When Downloading file "/drop/Alice/folder (2)/a.txt" Then Downloaded content should be "abc" + Scenario: File drop uploading file with name of folder + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as "Alice" + Then the HTTP status code should be "201" + When Dropping file "/folder" with "its a file" as "Alice" + Then the HTTP status code should be "201" + When Downloading file "/drop/Alice/folder/a.txt" + Then the HTTP status code should be "200" + And Downloaded content should be "abc" + When Downloading file "/drop/Alice/folder (2)" + Then the HTTP status code should be "200" + And Downloaded content should be "its a file" + Scenario: Put file same file multiple times via files drop Given user "user0" exists And As an "user0" @@ -86,7 +152,86 @@ Feature: FilesDrop | shareWith | | When Dropping file "/folder/a.txt" with "abc" as "Mallory" And Dropping file "/folder/a.txt" with "def" as "Mallory" - And Downloading file "/drop/Mallory/a.txt" + # Ensure folder structure and that we only checked + # for files duplicates, but merged the existing folders + Then as "user0" the folder "/drop/Mallory" exists + Then as "user0" the folder "/drop/Mallory/folder" exists + Then as "user0" the folder "/drop/Mallory (2)" does not exist + Then as "user0" the folder "/drop/Mallory/folder (2)" does not exist + Then as "user0" the file "/drop/Mallory/folder/a.txt" exists + Then as "user0" the file "/drop/Mallory/folder/a (2).txt" exists + And Downloading file "/drop/Mallory/folder/a.txt" Then Downloaded content should be "abc" - And Downloading file "/drop/Mallory/a (2).txt" + And Downloading file "/drop/Mallory/folder/a (2).txt" Then Downloaded content should be "def" + + Scenario: Files drop prevents GET + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | shareWith | | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + When Dropping file "/folder/a.txt" with "abc" as "Mallory" + When as "user0" the file "/drop/Mallory/folder/a.txt" exists + And Downloading public folder "Mallory" + Then the HTTP status code should be "405" + And Downloading public folder "Mallory/folder" + Then the HTTP status code should be "405" + And Downloading public file "Mallory/folder/a.txt" + Then the HTTP status code should be "405" + + Scenario: Files drop requires nickname if file request is enabled + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" + Then the HTTP status code should be "400" + + Scenario: Files request drop with invalid nickname with slashes + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as "Alice/Bob/Mallory" + Then the HTTP status code should be "400" + + Scenario: Files request drop with invalid nickname with forbidden characters + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as ".htaccess" + Then the HTTP status code should be "400" + + Scenario: Files request drop with invalid nickname with forbidden characters + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as ".Mallory" + Then the HTTP status code should be "400" diff --git a/build/integration/openldap_features/openldap-uid-username.feature b/build/integration/openldap_features/openldap-uid-username.feature index 9d5405e88bf..bee4098972b 100644 --- a/build/integration/openldap_features/openldap-uid-username.feature +++ b/build/integration/openldap_features/openldap-uid-username.feature @@ -151,6 +151,7 @@ Feature: LDAP | ldapAttributesForUserSearch | employeeNumber | | useMemberOfToDetectMembership | 1 | And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" + And invoking occ with "ldap:check-group cn=Orcharding,ou=OtherGroups,dc=nextcloud,dc=ci --update" And As an "alice" When getting sharees for # "5" is part of the employee number of some LDAP records @@ -162,4 +163,3 @@ Feature: LDAP And "users" sharees returned are | Elisa | 0 | elisa | And "exact groups" sharees returned is empty - diff --git a/build/integration/features/ratelimiting.feature b/build/integration/ratelimiting_features/ratelimiting.feature index a2fca2fc6be..43cfddec85d 100644 --- a/build/integration/features/ratelimiting.feature +++ b/build/integration/ratelimiting_features/ratelimiting.feature @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: AGPL-3.0-or-later +@RateLimiting Feature: ratelimiting Background: diff --git a/build/integration/routing_features/apps-and-routes.feature b/build/integration/routing_features/apps-and-routes.feature new file mode 100644 index 00000000000..954ea73bfac --- /dev/null +++ b/build/integration/routing_features/apps-and-routes.feature @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: appmanagement + Background: + Given using api version "2" + And user "user1" exists + And user "user2" exists + And group "group1" exists + And user "user1" belongs to group "group1" + + Scenario: Enable app and test route + Given As an "admin" + And sending "DELETE" to "/cloud/apps/weather_status" + And app "weather_status" is disabled + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "998" + And the HTTP status code should be "404" + When sending "POST" to "/cloud/apps/weather_status" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + And app "weather_status" is enabled + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + Given As an "user1" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + Given As an "user2" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + + Scenario: Enable app only for some groups + Given As an "admin" + And sending "DELETE" to "/cloud/apps/weather_status" + And app "weather_status" is disabled + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "998" + And the HTTP status code should be "404" + Given invoking occ with "app:enable weather_status --groups group1" + Then the command was successful + Given As an "user2" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the HTTP status code should be "412" + Given As an "user1" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + Given As an "admin" + And sending "DELETE" to "/cloud/apps/weather_status" + And app "weather_status" is disabled diff --git a/build/integration/run.sh b/build/integration/run.sh index 309f8ed0c75..30dd0646b10 100755 --- a/build/integration/run.sh +++ b/build/integration/run.sh @@ -18,10 +18,18 @@ HIDE_OC_LOGS=$2 INSTALLED=$($OCC status | grep installed: | cut -d " " -f 5) if [ "$INSTALLED" == "true" ]; then + # Disable appstore to avoid spamming from CI + $OCC config:system:set appstoreenabled --value=false --type=boolean # Disable bruteforce protection because the integration tests do trigger them $OCC config:system:set auth.bruteforce.protection.enabled --value false --type bool + # Disable rate limit protection because the integration tests do trigger them + $OCC config:system:set ratelimit.protection.enabled --value false --type bool # Allow local remote urls otherwise we can not share $OCC config:system:set allow_local_remote_servers --value true --type bool + # Allow self signed certificates + $OCC config:system:set sharing.federation.allowSelfSignedCertificates --value true --type bool + # Allow creating users with dummy passwords + $OCC app:disable password_policy else if [ "$SCENARIO_TO_RUN" != "setup_features/setup.feature" ]; then echo "Nextcloud instance needs to be installed" >&2 @@ -38,6 +46,7 @@ if [ -z "$EXECUTOR_NUMBER" ]; then fi PORT=$((8080 + $EXECUTOR_NUMBER)) echo $PORT +export PORT echo "" > "${NC_DATADIR}/nextcloud.log" echo "" > phpserver.log diff --git a/build/integration/sharing_features/sharing-activity.feature b/build/integration/sharing_features/sharing-activity.feature new file mode 100644 index 00000000000..016b376488b --- /dev/null +++ b/build/integration/sharing_features/sharing-activity.feature @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: sharing + Background: + Given using api version "1" + Given using new dav path + Given invoking occ with "app:enable --force activity" + Given the command was successful + Given user "user0" exists + And Logging in using web as "user0" + And Sending a "POST" to "/apps/activity/settings" with requesttoken + | public_links_notification | 1 | + | public_links_upload_notification | 1 | + | notify_setting_batchtime | 0 | + | activity_digest | 0 | + + Scenario: Creating a new mail share and check activity + Given dummy mail server is listening + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share can be downloaded + Then last activity should be + | app | files_sharing | + | type | public_links | + | object_type | files | + | object_name | /welcome.txt | + + Scenario: Creating a new public share and check activity + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last link share can be downloaded + Then last activity should be + | app | files_sharing | + | type | public_links | + | object_type | files | + | object_name | /welcome.txt | diff --git a/build/integration/sharing_features/sharing-v1-part2.feature b/build/integration/sharing_features/sharing-v1-part2.feature index 8cc97fe71ee..a6e4c67165a 100644 --- a/build/integration/sharing_features/sharing-v1-part2.feature +++ b/build/integration/sharing_features/sharing-v1-part2.feature @@ -543,6 +543,29 @@ Feature: sharing And the HTTP status code should be "200" And last share_id is included in the answer + Scenario: Group shares are deleted when the group is deleted + Given As an "admin" + And user "user0" exists + And user "user1" exists + And group "group0" exists + And user "user0" belongs to group "group0" + And file "textfile0.txt" of user "user1" is shared with group "group0" + And As an "user0" + When sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true" + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share_id is included in the answer + When group "group0" does not exist + Then sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true" + And the OCS status code should be "100" + And the HTTP status code should be "200" + And last share_id is not included in the answer + When group "group0" exists + Then sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true" + And the OCS status code should be "100" + And the HTTP status code should be "200" + And last share_id is not included in the answer + Scenario: User is not allowed to reshare file As an "admin" Given user "user0" exists @@ -701,6 +724,79 @@ Feature: sharing Then the OCS status code should be "404" And the HTTP status code should be "200" + Scenario: download restrictions can not be dropped + As an "admin" + Given user "user0" exists + And user "user1" exists + And user "user2" exists + And User "user0" uploads file with content "foo" to "/tmp.txt" + And As an "user0" + And creating a share with + | path | /tmp.txt | + | shareType | 0 | + | shareWith | user1 | + | permissions | 17 | + | attributes | [{"scope":"permissions","key":"download","value":false}] | + And As an "user1" + And accepting last share + When Getting info of last share + Then Share fields of last share match with + | uid_owner | user0 | + | uid_file_owner | user0 | + | permissions | 17 | + | attributes | [{"scope":"permissions","key":"download","value":false}] | + When creating a share with + | path | /tmp.txt | + | shareType | 0 | + | shareWith | user2 | + | permissions | 1 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + When As an "user2" + And accepting last share + And Getting info of last share + Then Share fields of last share match with + | share_type | 0 | + | permissions | 1 | + | uid_owner | user1 | + | uid_file_owner | user0 | + | attributes | [{"scope":"permissions","key":"download","value":false}] | + + Scenario: download restrictions can not be dropped when re-sharing even on link shares + As an "admin" + Given user "user0" exists + And user "user1" exists + And User "user0" uploads file with content "foo" to "/tmp.txt" + And As an "user0" + And creating a share with + | path | /tmp.txt | + | shareType | 0 | + | shareWith | user1 | + | permissions | 17 | + | attributes | [{"scope":"permissions","key":"download","value":false}] | + And As an "user1" + And accepting last share + When Getting info of last share + Then Share fields of last share match with + | uid_owner | user0 | + | attributes | [{"scope":"permissions","key":"download","value":false}] | + When creating a share with + | path | /tmp.txt | + | shareType | 3 | + | permissions | 1 | + And Getting info of last share + And Updating last share with + | hideDownload | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + When Getting info of last share + Then Share fields of last share match with + | share_type | 3 | + | uid_owner | user1 | + | uid_file_owner | user0 | + | hide_download | 1 | + | attributes | [{"scope":"permissions","key":"download","value":false}] | + Scenario: User is not allowed to reshare file with additional delete permissions As an "admin" Given user "user0" exists @@ -1169,7 +1265,9 @@ Feature: sharing |{http://open-collaboration-services.org/ns}share-permissions | Then the single response should contain a property "{http://open-collaboration-services.org/ns}share-permissions" with value "19" - Scenario: Cannot download a file when it's shared view-only + Scenario: Cannot download a file when it's shared view-only without shareapi_allow_view_without_download + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" moves file "/textfile0.txt" to "/document.odt" @@ -1178,8 +1276,15 @@ Feature: sharing When As an "user1" And Downloading file "/document.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And Downloading file "/document.odt" + Then the HTTP status code should be "200" - Scenario: Cannot download a file when its parent is shared view-only + Scenario: Cannot download a file when its parent is shared view-only without shareapi_allow_view_without_download + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" created a folder "/sharedviewonly" @@ -1189,8 +1294,15 @@ Feature: sharing When As an "user1" And Downloading file "/sharedviewonly/document.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And Downloading file "/sharedviewonly/document.odt" + Then the HTTP status code should be "200" - Scenario: Cannot copy a file when it's shared view-only + Scenario: Cannot copy a file when it's shared view-only even with shareapi_allow_view_without_download enabled + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" moves file "/textfile0.txt" to "/document.odt" @@ -1198,8 +1310,15 @@ Feature: sharing And user "user1" accepts last share When User "user1" copies file "/document.odt" to "/copyforbidden.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And User "user1" copies file "/document.odt" to "/copyforbidden.odt" + Then the HTTP status code should be "403" Scenario: Cannot copy a file when its parent is shared view-only + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" created a folder "/sharedviewonly" @@ -1208,5 +1327,10 @@ Feature: sharing And user "user1" accepts last share When User "user1" copies file "/sharedviewonly/document.odt" to "/copyforbidden.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And User "user1" copies file "/sharedviewonly/document.odt" to "/copyforbidden.odt" + Then the HTTP status code should be "403" # See sharing-v1-part3.feature diff --git a/build/integration/sharing_features/sharing-v1-part4.feature b/build/integration/sharing_features/sharing-v1-part4.feature index 1b32f99a0a2..d138f0a1769 100644 --- a/build/integration/sharing_features/sharing-v1-part4.feature +++ b/build/integration/sharing_features/sharing-v1-part4.feature @@ -124,3 +124,61 @@ Scenario: Receiving a share of a file without delete permission gives delete per | path | /welcome (2).txt | | permissions | 19 | | item_permissions | 27 | + +# This is a regression test as in the past creating a file drop required creating with permissions=5 +# and then afterwards update the share to permissions=4 +Scenario: Directly create link share with CREATE only permissions (file drop) + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/TMP" + When creating a share with + | path | TMP | + | shareType | 3 | + | permissions | 4 | + And Getting info of last share + Then Share fields of last share match with + | uid_file_owner | user0 | + | share_type | 3 | + | permissions | 4 | + +Scenario: Directly create email share with CREATE only permissions (file drop) + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/TMP" + When creating a share with + | path | TMP | + | shareType | 4 | + | shareWith | j.doe@example.com | + | permissions | 4 | + And Getting info of last share + Then Share fields of last share match with + | uid_file_owner | user0 | + | share_type | 4 | + | permissions | 4 | + +# This ensures the legacy behavior of sharing v1 is kept +Scenario: publicUpload overrides permissions + Given user "user0" exists + And As an "user0" + And parameter "outgoing_server2server_share_enabled" of app "files_sharing" is set to "no" + And user "user0" created a folder "/TMP" + When creating a share with + | path | TMP | + | shareType | 3 | + | permissions | 4 | + | publicUpload | true | + And Getting info of last share + Then Share fields of last share match with + | uid_file_owner | user0 | + | share_type | 3 | + | permissions | 15 | + When creating a share with + | path | TMP | + | shareType | 3 | + | permissions | 4 | + | publicUpload | false | + And Getting info of last share + Then Share fields of last share match with + | uid_file_owner | user0 | + | share_type | 3 | + | permissions | 1 | diff --git a/build/integration/sharing_features/sharing-v1.feature b/build/integration/sharing_features/sharing-v1.feature index 708667f123c..25f168db2e7 100644 --- a/build/integration/sharing_features/sharing-v1.feature +++ b/build/integration/sharing_features/sharing-v1.feature @@ -231,10 +231,11 @@ Feature: sharing | url | AN_URL | | mimetype | httpd/unix-directory | - Scenario: Creating a new share with expiration date removed, when default expiration is set + Scenario: Creating a new share with expiration date empty, when default expiration is set Given user "user0" exists And user "user1" exists - And parameter "shareapi_default_expire_date" of app "core" is set to "yes" + And parameter "shareapi_default_internal_expire_date" of app "core" is set to "yes" + And parameter "shareapi_internal_expire_after_n_days" of app "core" is set to "3" And As an "user0" When creating a share with | path | welcome.txt | @@ -249,6 +250,43 @@ Feature: sharing And Share fields of last share match with | expiration || + Scenario: Creating a new share with expiration date removed, when default expiration is set + Given user "user0" exists + And user "user1" exists + And parameter "shareapi_default_internal_expire_date" of app "core" is set to "yes" + And parameter "shareapi_internal_expire_after_n_days" of app "core" is set to "3" + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareWith | user1 | + | shareType | 0 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And Getting info of last share + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And Share fields of last share match with + | expiration | +3 days | + + Scenario: Creating a new share with expiration date null, when default expiration is set + Given user "user0" exists + And user "user1" exists + And parameter "shareapi_default_internal_expire_date" of app "core" is set to "yes" + And parameter "shareapi_internal_expire_after_n_days" of app "core" is set to "3" + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareWith | user1 | + | shareType | 0 | + | expireDate | null | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And Getting info of last share + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And Share fields of last share match with + | expiration | +3 days | + Scenario: Creating a new public share, updating its password and getting its info Given user "user0" exists And As an "user0" diff --git a/build/integration/theming_features/theming.feature b/build/integration/theming_features/theming.feature new file mode 100644 index 00000000000..2ae5d4f75c3 --- /dev/null +++ b/build/integration/theming_features/theming.feature @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: theming + + Background: + Given user "user0" exists + + Scenario: themed stylesheets are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + # Themes that can not be explicitly set by a guest could have been + # globally set too through "enforce_theme". + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed images are available for users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for guests + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for disabled users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" |