diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 345bee9eb..6a5cdec51 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -1,20 +1,120 @@ -name: CI - on: push: branches: - - "master" + - 'master' pull_request: schedule: - - cron: "0 22 * * *" # run at 10 PM UTC + - cron: '0 22 * * *' # run at 10 PM UTC + +name: CI concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - builds: - uses: ./.github/workflows/shared_workflow.yml - secrets: inherit - with: - branch: master + api-tests: + name: API tests + strategy: + matrix: + nextcloudVersion: [ stable28 ] + phpVersionMajor: [ 8 ] + phpVersionMinor: [ 1 ] + database: [mysql] + runs-on: ubuntu-latest + container: + image: ubuntu:latest + credentials: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + defaults: + run: + working-directory: integration_openproject + + services: + nextcloud: + image: ghcr.io/juliushaertl/nextcloud-dev-php${{ format('{0}{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }}:latest + env: + SQL: ${{ matrix.database }} + SERVER_BRANCH: ${{ matrix.nextcloudVersion }} + NEXTCLOUD_AUTOINSTALL: "YES" + NEXTCLOUD_AUTOINSTALL_APPS: "viewer activity groupfolders integration_openproject" + NEXTCLOUD_TRUSTED_DOMAINS: nextcloud + VIRTUAL_HOST: "nextcloud" + WITH_REDIS: "YES" + NEXTCLOUD_AUTOINSTALL_APPS_WAIT_TIME: 120 + volumes: + - /home/runner/work/integration_openproject/integration_openproject:/var/www/html/apps-shared + + database-postgres: + image: postgres:14 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: nextcloud + credentials: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + database-mysql: + image: mariadb:10.5 + env: + MYSQL_ROOT_PASSWORD: 'nextcloud' + MYSQL_PASSWORD: 'nextcloud' + MYSQL_USER: 'nextcloud' + MYSQL_DATABASE: 'nextcloud' + credentials: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + redis: + image: redis:7 + credentials: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + path: integration_openproject + + - name: Checkout activity app + uses: actions/checkout@v3 + with: + repository: nextcloud/activity + path: activity + ref: ${{ matrix.nextcloudVersion }} + + - name: Checkout groupfolders app + uses: actions/checkout@v3 + with: + repository: nextcloud/groupfolders + path: groupfolders + ref: ${{ matrix.nextcloudVersion }} + + - name: Setup PHP ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }} + tools: composer + extensions: intl + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache PHP dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/integration_openproject/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: API Tests + env: + NEXTCLOUD_BASE_URL: http://nextcloud + run: | + composer install --no-progress --prefer-dist --optimize-autoloader + until curl -s -f http://nextcloud/status.php | grep '"installed":true'; do echo .; sleep 10; done + make api-test diff --git a/.github/workflows/nighlty-ci-release-branch.yml b/.github/workflows/nighlty-ci-release-branch.yml index 3750d8272..979c176d0 100644 --- a/.github/workflows/nighlty-ci-release-branch.yml +++ b/.github/workflows/nighlty-ci-release-branch.yml @@ -1,12 +1,12 @@ -name: Nightly CI Release +# name: Nightly CI Release -on: - schedule: - - cron: '0 22 * * *' # run at 10 PM UTC +# on: +# schedule: +# - cron: '0 22 * * *' # run at 10 PM UTC -jobs: - builds: - uses: ./.github/workflows/shared_workflow.yml - secrets: inherit - with: - branch: release/2.4 +# jobs: +# builds: +# uses: ./.github/workflows/shared_workflow.yml +# secrets: inherit +# with: +# branch: release/2.4 diff --git a/.github/workflows/shared_workflow.yml b/.github/workflows/shared_workflow.yml index 649e3cc7d..547319a52 100644 --- a/.github/workflows/shared_workflow.yml +++ b/.github/workflows/shared_workflow.yml @@ -1,122 +1,122 @@ -on: - workflow_call: - inputs: - branch: - type: string +# on: +# workflow_call: +# inputs: +# branch: +# type: string -name: CI +# name: CI -jobs: - api-tests: - name: API tests - strategy: - matrix: - nextcloudVersion: [ stable28 ] - phpVersionMajor: [ 8 ] - phpVersionMinor: [ 1 ] - database: [mysql] - runs-on: ubuntu-20.04 - container: - image: ubuntu:latest - credentials: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} +# jobs: +# api-tests: +# name: API tests +# strategy: +# matrix: +# nextcloudVersion: [ stable28 ] +# phpVersionMajor: [ 8 ] +# phpVersionMinor: [ 1 ] +# database: [mysql] +# runs-on: ubuntu-20.04 +# container: +# image: ubuntu:latest +# credentials: +# username: ${{ secrets.DOCKERHUB_USERNAME }} +# password: ${{ secrets.DOCKERHUB_TOKEN }} - defaults: - run: - working-directory: integration_openproject +# defaults: +# run: +# working-directory: integration_openproject - services: - nextcloud: - image: ghcr.io/juliushaertl/nextcloud-dev-php${{ format('{0}{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }}:latest - env: - SQL: ${{ matrix.database }} - SERVER_BRANCH: ${{ matrix.nextcloudVersion }} - NEXTCLOUD_AUTOINSTALL: "YES" - NEXTCLOUD_AUTOINSTALL_APPS: "viewer activity groupfolders integration_openproject" - NEXTCLOUD_TRUSTED_DOMAINS: nextcloud - VIRTUAL_HOST: "nextcloud" - WITH_REDIS: "YES" - NEXTCLOUD_AUTOINSTALL_APPS_WAIT_TIME: 120 - volumes: - - /home/runner/work/integration_openproject/integration_openproject:/var/www/html/apps-shared +# services: +# nextcloud: +# image: ghcr.io/juliushaertl/nextcloud-dev-php${{ format('{0}{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }}:latest +# env: +# SQL: ${{ matrix.database }} +# SERVER_BRANCH: ${{ matrix.nextcloudVersion }} +# NEXTCLOUD_AUTOINSTALL: "YES" +# NEXTCLOUD_AUTOINSTALL_APPS: "viewer activity groupfolders integration_openproject" +# NEXTCLOUD_TRUSTED_DOMAINS: nextcloud +# VIRTUAL_HOST: "nextcloud" +# WITH_REDIS: "YES" +# NEXTCLOUD_AUTOINSTALL_APPS_WAIT_TIME: 120 +# volumes: +# - /home/runner/work/integration_openproject/integration_openproject:/var/www/html/apps-shared - database-postgres: - image: postgres:14 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: nextcloud - credentials: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} +# database-postgres: +# image: postgres:14 +# env: +# POSTGRES_PASSWORD: postgres +# POSTGRES_DB: nextcloud +# credentials: +# username: ${{ secrets.DOCKERHUB_USERNAME }} +# password: ${{ secrets.DOCKERHUB_TOKEN }} - database-mysql: - image: mariadb:10.5 - env: - MYSQL_ROOT_PASSWORD: 'nextcloud' - MYSQL_PASSWORD: 'nextcloud' - MYSQL_USER: 'nextcloud' - MYSQL_DATABASE: 'nextcloud' - credentials: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} +# database-mysql: +# image: mariadb:10.5 +# env: +# MYSQL_ROOT_PASSWORD: 'nextcloud' +# MYSQL_PASSWORD: 'nextcloud' +# MYSQL_USER: 'nextcloud' +# MYSQL_DATABASE: 'nextcloud' +# credentials: +# username: ${{ secrets.DOCKERHUB_USERNAME }} +# password: ${{ secrets.DOCKERHUB_TOKEN }} - redis: - image: redis:7 - credentials: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} +# redis: +# image: redis:7 +# credentials: +# username: ${{ secrets.DOCKERHUB_USERNAME }} +# password: ${{ secrets.DOCKERHUB_TOKEN }} - steps: - - name: Checkout For nightly Branch - if: github.event_name == 'schedule' - uses: actions/checkout@v3 - with: - path: integration_openproject - ref: ${{ inputs.branch }} +# steps: +# - name: Checkout For nightly Branch +# if: github.event_name == 'schedule' +# uses: actions/checkout@v3 +# with: +# path: integration_openproject +# ref: ${{ inputs.branch }} - - name: Checkout - if: github.event_name == 'pull_request' - uses: actions/checkout@v3 - with: - path: integration_openproject +# - name: Checkout +# if: github.event_name == 'pull_request' +# uses: actions/checkout@v3 +# with: +# path: integration_openproject - - name: Checkout activity app - uses: actions/checkout@v3 - with: - repository: nextcloud/activity - path: activity - ref: ${{ matrix.nextcloudVersion }} +# - name: Checkout activity app +# uses: actions/checkout@v3 +# with: +# repository: nextcloud/activity +# path: activity +# ref: ${{ matrix.nextcloudVersion }} - - name: Checkout groupfolders app - uses: actions/checkout@v3 - with: - repository: nextcloud/groupfolders - path: groupfolders - ref: ${{ matrix.nextcloudVersion }} +# - name: Checkout groupfolders app +# uses: actions/checkout@v3 +# with: +# repository: nextcloud/groupfolders +# path: groupfolders +# ref: ${{ matrix.nextcloudVersion }} - - name: Setup PHP ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }} - tools: composer - extensions: intl +# - name: Setup PHP ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }} +# uses: shivammathur/setup-php@v2 +# with: +# php-version: ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }} +# tools: composer +# extensions: intl - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT +# - name: Get composer cache directory +# id: composer-cache +# run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache PHP dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/integration_openproject/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- +# - name: Cache PHP dependencies +# uses: actions/cache@v3 +# with: +# path: ${{ steps.composer-cache.outputs.dir }} +# key: ${{ runner.os }}-composer-${{ hashFiles('**/integration_openproject/composer.lock') }} +# restore-keys: ${{ runner.os }}-composer- - - name: API Tests - env: - NEXTCLOUD_BASE_URL: http://nextcloud - run: | - composer install --no-progress --prefer-dist --optimize-autoloader - until curl -s -f http://nextcloud/status.php | grep '"installed":true'; do echo .; sleep 10; done - make api-test +# - name: API Tests +# env: +# NEXTCLOUD_BASE_URL: http://nextcloud +# run: | +# composer install --no-progress --prefer-dist --optimize-autoloader +# until curl -s -f http://nextcloud/status.php | grep '"installed":true'; do echo .; sleep 10; done +# make api-test diff --git a/tests/acceptance/features/api/capabilities.feature b/tests/acceptance/features/api/capabilities.feature deleted file mode 100644 index 7b3deef35..000000000 --- a/tests/acceptance/features/api/capabilities.feature +++ /dev/null @@ -1,48 +0,0 @@ -Feature: get capabilities of the app - - Scenario: Get capabilities when group folder app is enabled - When the administrator requests the nextcloud capabilities - Then the HTTP status code should be "200" - And the ocs data of the response should match - """" - { - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "type": "object", - "required": [ - "integration_openproject" - ], - "properties": { - "integration_openproject": { - "type": "object", - "required": [ - "app_version", - "groupfolder_version", - "groupfolders_enabled" - ], - "properties": { - "app_version": { - "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+$" - }, - "groupfolder_version": { - "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+(\\-\\w+)?$" - }, - "groupfolders_enabled": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - } - } - """ diff --git a/tests/acceptance/features/api/getFileinfoByFileIDAPI.feature b/tests/acceptance/features/api/getFileinfoByFileIDAPI.feature index de8f19899..552853684 100644 --- a/tests/acceptance/features/api/getFileinfoByFileIDAPI.feature +++ b/tests/acceptance/features/api/getFileinfoByFileIDAPI.feature @@ -1,5 +1,45 @@ Feature: retrieve file information of a single file, using the file ID + Scenario: valid setup without group folder + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "openproject_client_id": "the-client-id", + "openproject_client_secret": "the-client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": false, + "setup_app_password": false + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """" + { + "type": "object", + "required": [ + "nextcloud_oauth_client_name", + "openproject_redirect_uri", + "nextcloud_client_id", + "nextcloud_client_secret" + ], + "properties": { + "nextcloud_oauth_client_name": {"type": "string", "pattern": "^OpenProject client$"}, + "openproject_redirect_uri": {"type": "string", "pattern": "^http:\/\/some-host.de\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"type": "string", "pattern": "[A-Za-z0-9]+"}, + "nextcloud_client_secret": {"type": "string", "pattern": "[A-Za-z0-9]+"} + }, + "not": { + "required": [ + "openproject_revocation_status" + ] + } + } + """ + Scenario: get information of an existing file Given user "Carol" has been created And user "Carol" has uploaded file with content "some data" to "file.txt" diff --git a/tests/acceptance/features/api/getFilesinfoByFileIDsAPI.feature b/tests/acceptance/features/api/getFilesinfoByFileIDsAPI.feature index 8d51f93d3..f74e42556 100644 --- a/tests/acceptance/features/api/getFilesinfoByFileIDsAPI.feature +++ b/tests/acceptance/features/api/getFilesinfoByFileIDsAPI.feature @@ -1,5 +1,45 @@ Feature: retrieve information of multiple files using the file IDs + Scenario: valid setup without group folder + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "openproject_client_id": "the-client-id", + "openproject_client_secret": "the-client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": false, + "setup_app_password": false + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """" + { + "type": "object", + "required": [ + "nextcloud_oauth_client_name", + "openproject_redirect_uri", + "nextcloud_client_id", + "nextcloud_client_secret" + ], + "properties": { + "nextcloud_oauth_client_name": {"type": "string", "pattern": "^OpenProject client$"}, + "openproject_redirect_uri": {"type": "string", "pattern": "^http:\/\/some-host.de\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"type": "string", "pattern": "[A-Za-z0-9]+"}, + "nextcloud_client_secret": {"type": "string", "pattern": "[A-Za-z0-9]+"} + }, + "not": { + "required": [ + "openproject_revocation_status" + ] + } + } + """ + Scenario: get information of four files, group folders, one own, one received as share, one trashed, one not accessible Given user "Carol" has been created And group "grp1" has been created diff --git a/tests/acceptance/features/api/ydirectUpload.feature b/tests/acceptance/features/api/ydirectUpload.feature deleted file mode 100644 index e518cc0b4..000000000 --- a/tests/acceptance/features/api/ydirectUpload.feature +++ /dev/null @@ -1,893 +0,0 @@ -Feature: API endpoint for direct upload - - As an OpenProject user - I want to upload files to Nextcloud from inside OpenProject - So that I don't need to leave OpenProject, open Nextcloud and then search for the same work package to create a link. - - As an OpenProject admin - I want the OpenProject front-end to send the files directly to Nextcloud - So that the long requests for uploading don't block any resources on the OpenProject back-end. - - Background: - Given user "Carol" has been created - - - Scenario Outline: Send a file to the direct-upload endpoint - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | "" | - | data | some data | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "" for user "Carol" should be "some data" - Examples: - | valid-file-name | file-name | pattern | - | textfile0.txt | textfile0.txt | textfile0\\.txt | - | असजिलो file | असजिलो file | असजिलो file | - | ?&$%?§ file.txt | ?&$%?§ file.txt | \\?\\&\\$\\%\\?§ file\\.txt | - | ../textfile.txt | textfile.txt | textfile\\.txt | - | folder/testfile.txt | testfile.txt | testfile\\.txt | - | text\file.txt | file.txt | file\\.txt | - - - Scenario: Send an invalid filename to the direct-upload endpoint - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | " " | - | data | some data | - Then the HTTP status code should be "400" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^invalid file name$"} - } - } - """ - - Scenario: Send an empty filename to the direct-upload endpoint or exceed max_post_size - # we cannot distinguish both cases - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | "" | - | data | some data | - Then the HTTP status code should be "413" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error", - "upload_limit" - ], - "properties": { - "error": {"type": "string", "pattern": "^File was not uploaded\\. post_max_size exceeded\\?$"}, - "upload_limit": {"type": "integer"} - } - } - """ - - - Scenario: send a token that doesn't exist to the direct-upload endpoint - When an anonymous user sends a multipart form data POST request to the "direct-upload/4ojy3w2yqcMeqmfYMjJSfrr9n56wqJdPZPBdsSsiRD4A6SooKaQqqoKnpmGcFBiw" endpoint with: - | file_name | textfile.txt | - | data | some data | - Then the HTTP status code should be "401" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^unauthorized$"} - } - } - """ - - - Scenario: Send a file with a filename that already exists (no overwrite parameter) - Given user "Carol" has uploaded file with content "original data" to "/file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | changed data | - Then the HTTP status code should be "409" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^conflict, file name already exists$"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "original data" - - - Scenario: Folder is deleted before upload happens - Given user "Carol" has created folder "/forOP" - And user "Carol" got a direct-upload token for "/forOP" - And user "Carol" has deleted folder "/forOP" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "404" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^folder not found or not enough permissions$"} - } - } - """ - - - Scenario: Folder is deleted and recreated (new fileid) before upload happens - Given user "Carol" has created folder "/forOP" - And user "Carol" got a direct-upload token for "/forOP" - And user "Carol" has deleted folder "/forOP" - And user "Carol" has created folder "/forOP" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "404" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^folder not found or not enough permissions$"} - } - } - """ - - - Scenario Outline: Folder is renamed before upload happens - Given user "Carol" has created folder "/forOP" - And user "Carol" has created folder "/secondfolder" - And user "Carol" got a direct-upload token for "/forOP" - And user "Carol" has renamed folder "/forOP" to "" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "some data" - Examples: - | rename-destination | - | /renamed | - | /secondfolder/forOP | - - - Scenario: Upload to a folder that is received by different routes - Given user "Brian" has been created - And user "Chandra" has been created - And user "Dipak" has been created - And user "Brian" has created folder "/toShare" - And user "Brian" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Brian" has shared folder "/toShare" with user "Chandra" with "all" permissions - And user "Brian" has shared folder "/toShare" with user "Dipak" with "all" permissions - And user "Chandra" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Dipak" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Carol" got a direct-upload token for "/toShare" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - - Scenario: Use the same token after one successful upload - Given user "Carol" got a direct-upload token for "/" - And an anonymous user has sent a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | testfile.txt | - | data | some data | - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "401" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^unauthorized$"} - } - } - """ - - Scenario: Use the same token after one unsuccessful upload - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | "" | - | data | some data | - Then the HTTP status code should be "413" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "401" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^unauthorized$"} - } - } - """ - - Scenario: use a token created by a user that was disabled after creating the token - Given user "Carol" got a direct-upload token for "/" - And user "Carol" has been disabled - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "401" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^unauthorized$"} - } - } - """ - - - Scenario: use a token created by a user that was deleted after creating the token - Given user "Carol" got a direct-upload token for "/" - And user "Carol" has been deleted - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "401" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^unauthorized$"} - } - } - """ - - - Scenario: use a token created by a user that was deleted and recreated after creating the token - Given user "Carol" got a direct-upload token for "/" - And user "Carol" has been deleted - And user "Carol" has been created - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | some data | - Then the HTTP status code should be "404" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^folder not found or not enough permissions$"} - } - } - """ - - - Scenario: send file to a share without create permissions - Given user "Brian" has been created - And user "Brian" has created folder "/toShare" - And user "Brian" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Carol" got a direct-upload token for "/toShare" - And user "Brian" has changed the share permissions of last created share to "read+update+delete+share" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - Then the HTTP status code should be "403" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^not enough permissions$"} - } - } - """ - - Scenario: overwrite a file to a share without create but with update permissions - Given user "Brian" has been created - And user "Brian" has created folder "/toShare" - And user "Brian" has uploaded file with content "original data" to "/toShare/file.txt" - And user "Brian" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Carol" got a direct-upload token for "/toShare" - And user "Brian" has changed the share permissions of last created share to "read+update+delete+share" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | true | - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/toShare/file.txt" for user "Carol" should be "new data" - And the content of file at "/toShare/file.txt" for user "Brian" should be "new data" - - - Scenario: set overwrite to false and send file with an existing filename - Given user "Carol" has uploaded file with content "original data" to "/file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | false | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file \\(2\\)\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "original data" - And the content of file at "/file (2).txt" for user "Carol" should be "new data" - - - Scenario: set overwrite to false and send file with an existing filename, also files with that name and suffixed numbers also exist - Given user "Carol" has uploaded file with content "data 1" to "/file.txt" - And user "Carol" has uploaded file with content "data 2" to "/file (2).txt" - And user "Carol" has uploaded file with content "data 3" to "/file (3).txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | false | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file \\(4\\)\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "data 1" - And the content of file at "/file (2).txt" for user "Carol" should be "data 2" - And the content of file at "/file (3).txt" for user "Carol" should be "data 3" - And the content of file at "/file (4).txt" for user "Carol" should be "new data" - - - Scenario: set overwrite to false and send file with an existing filename (filename has already a number in brackets) - Given user "Carol" has uploaded file with content "original data" to "/file (2).txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file (2).txt | - | data | new data | - | overwrite | false | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file \\(3\\)\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file (2).txt" for user "Carol" should be "original data" - And the content of file at "/file (3).txt" for user "Carol" should be "new data" - - - Scenario: set overwrite to true and send file with an existing filename - Given user "Carol" has uploaded file with content "original data" to "/file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | true | - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "new data" - - - Scenario: set overwrite to true and send file with an existing filename, but no permissions to overwrite - Given user "Brian" has been created - And user "Brian" has uploaded file with content "original data" to "/file.txt" - And user "Brian" has shared file "/file.txt" with user "Carol" with "read" permissions - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | true | - Then the HTTP status code should be "403" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^not enough permissions$"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "original data" - - - Scenario Outline: set overwrite to an invalid value - Given user "Carol" has uploaded file with content "original data" to "/file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | | - Then the HTTP status code should be "400" - And the data of the response should match - """" - { - "type": "object", - "not": { - "required": [ - "file_name", - "file_id" - ] - }, - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^invalid overwrite value$"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "original data" - Examples: - | overwrite | - | 1 | - | 0 | - | null | - | | - | rubbish | - - - Scenario: CORS preflight request - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends an OPTIONS request to the "direct-upload/%last-created-direct-upload-token%" endpoint with these headers: - | header | value | - | Access-Control-Request-Method | POST | - | Access-Control-Request-Headers | origin, x-requested-with | - | Origin | https://openproject.org | - Then the HTTP status code should be "200" - And the following headers should be set - | header | value | - | Access-Control-Allow-Origin | https://openproject.org | - | Access-Control-Allow-Methods | POST | - - - Scenario Outline: set overwrite and send a new file - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "new data" - Examples: - | overwrite | - | true | - | false | - - - Scenario: set overwrite to true and send a file with an existing folder name - Given user "Carol" has created folder "file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | true | - Then the HTTP status code should be "409" - And the data of the response should match - """" - { - "type": "object", - "not": { - "required": [ - "file_name", - "file_id" - ] - }, - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^overwrite is not allowed on non-files$"} - } - } - """ - - - Scenario: set overwrite to false and send a file with an existing folder name - Given user "Carol" has created folder "file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - | overwrite | false | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file \\(2\\)\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file (2).txt" for user "Carol" should be "new data" - - - Scenario: don't set overwrite and send a file with an existing folder name - Given user "Carol" has created folder "file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | new data | - Then the HTTP status code should be "409" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^conflict, file name already exists$"} - } - } - """ - - - Scenario: Upload a file that just fits into the users quota - Given the quota of user "Carol" has been set to "10 B" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | textfile0.txt | - | data | 1234567890 | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^textfile0\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "textfile0.txt" for user "Carol" should be "1234567890" - - - Scenario: Upload a file exceeding the users quota - Given the quota of user "Carol" has been set to "9 B" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | 1234567890 | - Then the HTTP status code should be "507" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^insufficient quota$"} - } - } - """ - - - Scenario: Upload a file into a folder, exceeding the users quota - Given the quota of user "Carol" has been set to "9 B" - And user "Carol" has created folder "/forOP" - And user "Carol" got a direct-upload token for "/forOP" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | 1234567890 | - Then the HTTP status code should be "507" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^insufficient quota$"} - } - } - """ - - - Scenario: Upload a file into a shared folder exceeding the quota of the user sharing the folder - Given user "Brian" has been created - And the quota of user "Carol" has been set to "10 B" - And the quota of user "Brian" has been set to "9 B" - And user "Brian" has created folder "/toShare" - And user "Brian" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Carol" got a direct-upload token for "/toShare" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | 1234567890 | - Then the HTTP status code should be "507" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^insufficient quota$"} - } - } - """ - - - Scenario: Upload a file into a shared folder exceeding the quota of sharee but not that of sharer - Given user "Brian" has been created - And the quota of user "Carol" has been set to "10 B" - And the quota of user "Brian" has been set to "20 B" - And user "Brian" has created folder "/toShare" - And user "Brian" has shared folder "/toShare" with user "Carol" with "all" permissions - And user "Carol" got a direct-upload token for "/toShare" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | 123456789012345 | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/toShare/file.txt" for user "Carol" should be "123456789012345" - And the content of file at "/toShare/file.txt" for user "Brian" should be "123456789012345" - - - Scenario: overwrite an existing file with content that fits the quota. Needed quota is sizeof(old data)+sizeof(new data) - Given the quota of user "Carol" has been set to "20 B" - And user "Carol" has uploaded file with content "1234567890" to "/file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | 0987654321 | - | overwrite | true | - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^file\\.txt$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "0987654321" - - - Scenario: try to overwrite an existing file with content that exceeds the quota. Needed quota is sizeof(old data)+sizeof(new data) - Given the quota of user "Carol" has been set to "19 B" - And user "Carol" has uploaded file with content "1234567890" to "/file.txt" - And user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | file.txt | - | data | 0987654321 | - | overwrite | true | - Then the HTTP status code should be "507" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^insufficient quota$"} - } - } - """ - And the content of file at "/file.txt" for user "Carol" should be "1234567890" - - - Scenario: Try to upload a file with a blacklisted file name - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | .htaccess | - | data | | - Then the HTTP status code should be "403" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^invalid file name$"} - } - } - """ - - - Scenario: upload a hidden file - Given user "Carol" got a direct-upload token for "/" - When an anonymous user sends a multipart form data POST request to the "direct-upload/%last-created-direct-upload-token%" endpoint with: - | file_name | .hidden | - | data | hidden file | - Then the HTTP status code should be "201" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "file_name", - "file_id" - ], - "properties": { - "file_name": {"type": "string", "pattern": "^\\.hidden$"}, - "file_id": {"type" : "integer"} - } - } - """ - And the content of file at "/.hidden" for user "Carol" should be "hidden file" - - diff --git a/tests/acceptance/features/api/ydirectUploadPrepare.feature b/tests/acceptance/features/api/ydirectUploadPrepare.feature deleted file mode 100644 index 34d13c6bc..000000000 --- a/tests/acceptance/features/api/ydirectUploadPrepare.feature +++ /dev/null @@ -1,157 +0,0 @@ -Feature: API endpoint to prepare direct upload - - As an OpenProject user - I want to upload files to Nextcloud from inside OpenProject - So that I don't need to leave OpenProject, open Nextcloud and then search for the same work package to create a link. - - As an OpenProject admin - I want the OpenProject front-end to send the files directly to Nextcloud - So that the long requests for uploading don't block any resources on the OpenProject back-end. - - Background: - Given user "Carol" has been created - - Scenario Outline: Get a direct-upload token for a folder - Given user "Carol" has created folder - When user "Carol" sends a POST request to the direct-upload-token endpoint with the ID of - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "token", - "expires_on" - ], - "properties": { - "token": {"type": "string", "pattern": "^[A-Za-z0-9]{64}$"}, - "expires_on" : {"type" : "integer", "minimum": %now+3500s%, "maximum": %now+3600s%} - } - } - """ - Examples: - | folder | - | "/forOpenProject" | - | "/folder with spaces" | - | "/a/sub/folder" | - | "/असजिलो folder" | - | "/?&$%?§ folder" | - - - Scenario: Try to get a direct-upload token for the root folder - When user "Carol" sends a POST request to the direct-upload-token endpoint with the ID of "/" - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "token", - "expires_on" - ], - "properties": { - "token": {"type": "string", "pattern": "^[A-Za-z0-9]{64}$"}, - "expires_on" : {"type" : "integer", "minimum": %now+3500s%, "maximum": %now+3600s%} - } - } - """ - - - Scenario Outline: Get a direct-upload token for a folder received as share - Given user "Brian" has been created - And user "Brian" has created folder "/toShare" - And user "Brian" has shared folder "/toShare" with user "Carol" with "" permissions - When user "Carol" sends a POST request to the direct-upload-token endpoint with the ID of "/toShare" - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "token", - "expires_on" - ], - "properties": { - "token": {"type": "string", "pattern": "^[A-Za-z0-9]{64}$"}, - "expires_on" : {"type" : "integer", "minimum": %now+3500s%, "maximum": %now+3600s%} - } - } - """ - Examples: - | permissions | - | all | - | read+update+create+delete | - | read+update+create+share | - | read+create+delete+share | - | read+create | - - - Scenario: Try to get a direct-upload token for a file - Given user "Carol" has uploaded file with content "some data" to "/file.txt" - When user "Carol" sends a POST request to the direct-upload-token endpoint with the ID of "/file.txt" - Then the HTTP status code should be "404" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^folder not found or not enough permissions$"} - } - } - """ - - - Scenario: Try to get a direct-upload token for a non existing folder-id - When user "Carol" sends a POST request to the direct-upload-token endpoint with the ID "999999999" - Then the HTTP status code should be "404" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^folder not found or not enough permissions$"} - } - } - """ - - - Scenario: Try to get a direct-upload token for a folder without create permissions - Given user "Brian" has been created - And user "Brian" has created folder "/toShare" - And user "Brian" has shared folder "/toShare" with user "Carol" with "read+update+delete+share" permissions - When user "Carol" sends a POST request to the direct-upload-token endpoint with the ID of "/toShare" - Then the HTTP status code should be "404" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": {"type": "string", "pattern": "^folder not found or not enough permissions$"} - } - } - """ - - - Scenario: Try to get token as non-existent user - When user "test" sends a POST request to the direct-upload-token endpoint with the ID "123" - Then the HTTP status code should be "401" - - - Scenario: Tokens should be random - Given user "Brian" has been created - And user "Carol" has created folder "/folder for OpenProject" - And user "Brian" has created folder "/folder for OpenProject" - When user "Carol" gets a direct-upload token for "/folder for OpenProject" - And user "Brian" gets a direct-upload token for "/folder for OpenProject" - And user "Carol" gets a direct-upload token for "/" - And user "Brian" gets a direct-upload token for "/" - Then all direct-upload tokens should be different