diff --git a/tests/acceptance/features/api/directUpload.feature b/tests/acceptance/features/api/directUpload.feature index 822258284..321c87c13 100644 --- a/tests/acceptance/features/api/directUpload.feature +++ b/tests/acceptance/features/api/directUpload.feature @@ -678,7 +678,7 @@ Feature: API endpoint for direct upload } """ - @expect-fail-on-nc34 @expect-fail-on-ncmaster + @expect-fail-on-nc34 @expect-fail-on-ncmaster @expect-fail-on-nc32 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 "/" diff --git a/tests/acceptance/features/api/oauthSetup.feature b/tests/acceptance/features/api/oauthSetup.feature new file mode 100644 index 000000000..81764515e --- /dev/null +++ b/tests/acceptance/features/api/oauthSetup.feature @@ -0,0 +1,607 @@ +# SPDX-FileCopyrightText: 2022-2024 Jankari Tech Pvt. Ltd. +# SPDX-License-Identifier: AGPL-3.0-or-later + +@integration-setup +Feature: setup the integration with OAuth method + + + Scenario: setup without team 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": {"const": "OpenProject client"}, + "openproject_redirect_uri": {"pattern": "^http:\/\/some-host.de\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"pattern": "[A-Za-z0-9]+"}, + "nextcloud_client_secret": {"pattern": "[A-Za-z0-9]+"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + + + Scenario Outline: try to setup with invalid data + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": , + "openproject_client_id": , + "openproject_client_secret": , + "default_enable_navigation": , + "default_enable_unified_search": , + "setup_project_folder": , + "setup_app_password": + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid data"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | instance_url | openproject_client_id | openproject_client_secret | enable_navigation | enable_unified_search | setup_project_folder | setup_app_password | + | null | null | null | null | null | null | null | + | null | "id" | "secret" | false | false | false | false | + | "http://some-host.de" | null | "secret" | false | false | false | false | + | "http://some-host.de" | "id" | null | false | false | false | false | + | "" | "id" | "secret" | false | false | false | false | + | "http://some-host.de" | "" | "secret" | false | false | false | false | + | "http://some-host.de" | "id" | "" | false | false | false | false | + | "ftp://somehost.de" | "the-id" | "secret" | true | false | "a string" | "a string" | + | "http://somehost.de" | false | "secret" | true | false | false | false | + | "http://somehost.de" | "id" | false | true | false | false | false | + + + Scenario: try to setup with invalid keys + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "instance_url": "http://openproject.de", + "client_id": "the-client" + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid key"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + + + Scenario Outline: try to setup with missing keys + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid key"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | values | + | {"openproject_client_id": "the-client-id", "openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "default_enable_unified_search": false} | + | {"openproject_instance_url": "http://some-host.de","openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "default_enable_unified_search": false, "setup_project_folder": false, "setup_app_password": false } | + | {"openproject_instance_url": "http://some-host.de", "openproject_client_id": "the-client-id", "default_enable_navigation": false, "default_enable_unified_search": false, "setup_project_folder": false, "setup_app_password": false} | + | {"openproject_instance_url": "http://some-host.de", "openproject_client_id": "the-client-id", "openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "setup_project_folder": false , "setup_app_password": false} | + + + Scenario Outline: try to setup with invalid json data + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + + """ + Then the HTTP status code should be "400" or "500" + Examples: + | data | + | "{}" | + | null | + | {"values": | + | "values" | + | "" | + + + Scenario: non-admin user tries to setup without team folder + Given user "Carol" has been created + When the user "Carol" 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 "403" + And the data of the response should match + """ + { + "type": "object", + "required": ["message"], + "properties": { + "message": {"const": "Logged in account must be an admin"} + }, + "not": { + "required": [ + "nextcloud_oauth_client_name", + "openproject_redirect_uri", + "nextcloud_client_id", + "nextcloud_client_secret", + "openproject_revocation_status" + ] + } + } + """ + + + Scenario Outline: update settings + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "openproject_client_id": "client-id", + "openproject_client_secret": "client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": false, + "setup_app_password": false + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values": { + + } + } + """ + 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" + ], + "properties": { + "nextcloud_oauth_client_name": {"const": "OpenProject client"}, + "openproject_redirect_uri": {"pattern": "^http:\/\/.*\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"pattern": "[A-Za-z0-9]+"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | settings | + | "openproject_instance_url":"http://some-new-host.de" | + | "openproject_client_id":"new-client","openproject_client_secret":"secret-value" | + | "default_enable_navigation":false,"default_enable_unified_search":true | + + + Scenario Outline: try to update a setting with invalid data + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "openproject_client_id": "client-id", + "openproject_client_secret": "client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": false, + "setup_app_password": false + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values": { + + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": ""} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | settings | error-message | + | "openproject_instance_url":false | invalid data | + | "openproject_client_id":"clientid","default_enable_navigation": "string" | invalid data | + | "openproject_client_secret":"" | invalid data | + | "default_enable_navigation":false,"default_enable_unified_search":null | invalid data | + | "instance_url":"http://op.de" | invalid key | + + + Scenario Outline: try to update settings with invalid json data + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "openproject_client_id": "client-id", + "openproject_client_secret": "client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": false, + "setup_app_password": false + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + + """ + Then the HTTP status code should be "400" or "500" + Examples: + | data | + | null | + | { "values": | + | "{}" | + | "" | + + + Scenario: non-admin user tries to update the settings + Given user "Carol" has been created + When the user "Carol" sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de" + } + } + """ + Then the HTTP status code should be "403" + And the data of the response should match + """ + { + "type": "object", + "required": ["message"], + "properties": { + "message": {"const": "Logged in account must be an admin"} + }, + "not": { + "required": [ + "nextcloud_oauth_client_name", + "openproject_redirect_uri", + "nextcloud_client_id", + "nextcloud_client_secret", + "openproject_revocation_status" + ] + } + } + """ + + + Scenario: reset the integration setup + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "openproject_client_id": "client-id", + "openproject_client_secret": "client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": false, + "setup_app_password": false + } + } + """ + When the administrator sends a DELETE request to the "setup" endpoint + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": ["status"], + "properties": { + "status": {"const": true} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + + + Scenario: non-admin user tries to reset the integration setup + Given user "Carol" has been created + When the user "Carol" sends a DELETE request to the "setup" endpoint + Then the HTTP status code should be "403" + + Scenario Outline: try to setup with incomplete team 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": , + "setup_app_password": + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid data"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | setup_project_folder | setup_app_password | + | true | false | + | false | true | + + + Scenario: Set up whole integration with project folder and user app password + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oauth2", + "openproject_client_id": "the-client-id", + "openproject_client_secret": "the-client-secret", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": true, + "setup_app_password": true + } + } + """ + 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", + "openproject_user_app_password" + ], + "properties": { + "nextcloud_oauth_client_name": {"const": "OpenProject client"}, + "openproject_redirect_uri": {"pattern": "^http:\/\/some-host.de\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"pattern": "[A-Za-z0-9]+"}, + "nextcloud_client_secret": {"pattern": "[A-Za-z0-9]+"}, + "openproject_user_app_password": {"pattern": "[A-Za-z0-9]+"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + And user "OpenProject" should be present in the server + And group "OpenProject" should be present in the server + And user "OpenProject" should be the subadmin of the group "OpenProject" + And group "OpenProjectNoAutomaticProjectFolders" should be present in the server + And user "OpenProject" should be the subadmin of the group "OpenProjectNoAutomaticProjectFolders" + And groupfolder "OpenProject" should be present in the server + And groupfolder "OpenProject" should be assigned to the group "OpenProject" with all permissions + And groupfolder "OpenProject" should have advance permissions enabled + And groupfolder "OpenProject" should be managed by the user "OpenProject" + And user "OpenProject" should have a folder called "OpenProject" + # folders inside the OpenProject folder can only be deleted/renamed by the OpenProject user + Given user "Carol" has been created + And user "Carol" has been added to the group "OpenProject" + And user "OpenProject" has created folder "/OpenProject/project-abc" + Then user "Carol" should have a folder called "OpenProject/project-abc" + When user "Carol" deletes folder "/OpenProject/project-abc" + Then the HTTP status code should be 500 + When user "Carol" renames folder "/OpenProject/project-abc" to "/OpenProject/project-123" + Then the HTTP status code should be 500 + When user "OpenProject" renames folder "/OpenProject/project-abc" to "/OpenProject/project-123" + Then the HTTP status code should be 201 + When user "OpenProject" deletes folder "/OpenProject/project-123" + Then the HTTP status code should be 204 + + # folders 2 levels down inside the OpenProject folder can be deleted by any user even if the parent is also called "OpenProject" + Given user "OpenProject" has created folder "/OpenProject/OpenProject/project-abc" + When user "Carol" renames folder "/OpenProject/OpenProject/project-abc" to "/OpenProject/OpenProject/project-123" + Then the HTTP status code should be 201 + When user "Carol" deletes folder "/OpenProject/OpenProject/project-123" + Then the HTTP status code should be 204 + + # a user, who is not in the OpenProject group can delete/rename items inside a folder that is called OpenProject + Given user "Brian" has been created + And user "Brian" has created folder "/OpenProject/project-abc" + When user "Brian" renames folder "/OpenProject/project-abc" to "/OpenProject/project-123" + Then the HTTP status code should be 201 + When user "Brian" deletes folder "/OpenProject/project-123" + Then the HTTP status code should be 204 + + # check deleting / disabling the OpenProject user/group + When the administrator deletes the user "OpenProject" + Then the HTTP status code should be 400 + And user "OpenProject" should be present in the server + When the administrator deletes the group "OpenProject" + Then the HTTP status code should be 400 + And group "OpenProject" should be present in the server + When the administrator disables the user "OpenProject" + Then the HTTP status code should be 400 + + # resending setup request will fail + 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": true, + "setup_app_password": true + } + } + """ + Then the HTTP status code should be "409" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "The user \"OpenProject\" already exists"} + } + } + """ + + # sending a PATCH request with setup_project_folder=true will also fail + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values" : { + "setup_project_folder": true + } + } + """ + Then the HTTP status code should be "409" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "The user \"OpenProject\" already exists"} + } + } + """ + + # we can make api request using the created app password for user "OpenProject" + When user "OpenProject" sends a "PROPFIND" request to "/remote.php/webdav" using current app password + Then the HTTP status code should be "207" + + # this is to provide test coverage for issues like this + # https://community.openproject.org/projects/nextcloud-integration/work_packages/49621 + When a new browser session for "Openproject" starts + # but other values can be updated by sending a PATCH request + # also we can replace old app password by sending PATCH request to get new user app password + And the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values" : { + "default_enable_navigation": true, + "setup_app_password": true + } + } + """ + 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" + ], + "properties": { + "nextcloud_oauth_client_name": {"const": "OpenProject client"}, + "openproject_redirect_uri": {"pattern": "^http:\/\/some-host.de\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"pattern": "[A-Za-z0-9]+"}, + "openproject_user_app_password": {"pattern": "[A-Za-z0-9]+"} + } + } + """ + And the newly generated app password should be different from the previous one + + # user "OpenProject" can make api request using the newly created app password + When user "OpenProject" sends a "PROPFIND" request to "/remote.php/webdav" using new app password + Then the HTTP status code should be "207" + + # user "OpenProject" cannot make api request using the old app password + When user "OpenProject" sends a "PROPFIND" request to "/remote.php/webdav" using old app password + Then the HTTP status code should be "401" diff --git a/tests/acceptance/features/api/oidcSetup.feature b/tests/acceptance/features/api/oidcSetup.feature new file mode 100644 index 000000000..72f245a76 --- /dev/null +++ b/tests/acceptance/features/api/oidcSetup.feature @@ -0,0 +1,622 @@ +# SPDX-FileCopyrightText: 2026 Jankari Tech Pvt. Ltd. +# SPDX-License-Identifier: AGPL-3.0-or-later + +@integration-setup +Feature: setup the integration with OIDC method + + + Scenario: setup without team folder (External IDP with token exchange) + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "external", + "oidc_provider": "Keycloak", + "token_exchange": true, + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": ["status"], + "properties": { + "status": {"const": true} + }, + "not": { + "required": ["nextcloud_oauth_client_name"] + } + } + """ + + + Scenario: setup without team folder (External IDP without token exchange) + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "external", + "oidc_provider": "Keycloak", + "token_exchange": false, + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": ["status"], + "properties": { + "status": {"const": true} + }, + "not": { + "required": ["nextcloud_oauth_client_name"] + } + } + """ + + + Scenario: setup without team folder (Nextcloud IDP) + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": ["status"], + "properties": { + "status": {"const": true} + }, + "not": { + "required": ["nextcloud_oauth_client_name"] + } + } + """ + + + Scenario Outline: try to setup with invalid data + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": , + "sso_provider_type": , + "targeted_audience_client_id": , + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": ""} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | auth_method | provider_type | target_client_id | error | + | null | "nextcloud_hub" | "client-id" | Invalid authorization method | + | "" | "nextcloud_hub" | "client-id" | Invalid authorization method | + | true | "nextcloud_hub" | "client-id" | Invalid authorization method | + | "unknown" | "nextcloud_hub" | "client-id" | Invalid authorization method | + | "oidc" | null | "client-id" | invalid key | + | "oidc" | "" | "client-id" | invalid key | + | "oidc" | true | "client-id" | invalid key | + | "oidc" | "unknown" | "client-id" | invalid key | + | "oidc" | "nextcloud_hub" | null | invalid data | + | "oidc" | "nextcloud_hub" | "" | invalid data | + | "oidc" | "nextcloud_hub" | false | invalid data | + | "oidc" | "nextcloud_hub" | [] | invalid data | + + + Scenario: try to setup with unknown key + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://openproject.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "client-id", + "setup_project_folder": false, + "setup_app_password": false, + "unknown_key": "some-value" + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid key"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + + + Scenario Outline: try to setup with missing keys + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://openproject.de", + "authorization_method": "oidc", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false, + + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": ""} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | settings | error | + | "targeted_audience_client_id":"client-id" | Incomplete settings: 'sso_provider_type' is required with 'oidc' method | + | "sso_provider_type":"nextcloud_hub" | invalid key | + | "sso_provider_type":"external", "token_exchange":false | invalid key | + | "sso_provider_type":"external", "oidc_provider":"test", "token_exchange":true | invalid key | + + + Scenario: non-admin user tries to setup without team folder + Given user "Carol" has been created + When the user "Carol" sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + Then the HTTP status code should be "403" + And the data of the response should match + """ + { + "type": "object", + "required": ["message"], + "properties": { + "message": {"const": "Logged in account must be an admin"} + }, + "not": { + "required": [ + "nextcloud_oauth_client_name", + "openproject_redirect_uri", + "nextcloud_client_id", + "nextcloud_client_secret", + "openproject_revocation_status" + ] + } + } + """ + + + Scenario Outline: update settings + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values": { + + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": ["status"], + "properties": { + "status": {"const": true} + }, + "not": { + "required": ["nextcloud_oauth_client_name"] + } + } + """ + Examples: + | settings | + | "targeted_audience_client_id":"new-id" | + | "sso_provider_type":"external","oidc_provider":"keycloak","token_exchange":false | + | "sso_provider_type":"external","token_exchange":true,"targeted_audience_client_id":"client-id" | + + + Scenario Outline: try to update a setting with invalid data + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values": { + + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid data"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | settings | + | "sso_provider_type":"unknown" | + | "sso_provider_type":"external","oidc_provider":false,"token_exchange":false | + | "sso_provider_type":"external","token_exchange":"true","targeted_audience_client_id":"client-id" | + + + Scenario Outline: try to update settings with invalid json data + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + + """ + Then the HTTP status code should be "400" or "500" + Examples: + | data | + | { "values": { "openproject_instance_url": "http://some-host.de"} }} | + | { "values": { "openproject_instance_url": "http://some-host.de"} | + | "values": { "openproject_instance_url": "http://some-host.de"} } | + | { "values": | + | "{}" | + | "" | + + + Scenario: non-admin user tries to update the settings + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + And user "Carol" has been created + When the user "Carol" sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de" + } + } + """ + Then the HTTP status code should be "403" + And the data of the response should match + """ + { + "type": "object", + "required": ["message"], + "properties": { + "message": {"const": "Logged in account must be an admin"} + }, + "not": { + "required": [ + "nextcloud_oauth_client_name", + "openproject_redirect_uri", + "nextcloud_client_id", + "nextcloud_client_secret", + "openproject_revocation_status" + ] + } + } + """ + + + Scenario: reset the integration setup + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + When the administrator sends a DELETE request to the "setup" endpoint + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": ["status"], + "properties": { + "status": {"const": true} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + + + Scenario: non-admin user tries to reset the integration setup + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": false, + "setup_app_password": false, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + And user "Carol" has been created + When the user "Carol" sends a DELETE request to the "setup" endpoint + Then the HTTP status code should be "403" + + + Scenario Outline: try to setup with incomplete team folder + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": , + "setup_app_password": + } + } + """ + Then the HTTP status code should be "400" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "invalid data"} + }, + "not": { + "required": ["openproject_revocation_status"] + } + } + """ + Examples: + | setup_project_folder | setup_app_password | + | true | false | + | false | true | + + + Scenario: setup with team folder + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": true, + "setup_app_password": true + } + } + """ + Then the HTTP status code should be "200" + And the data of the response should match + """ + { + "type": "object", + "required": [ + "status", + "openproject_user_app_password" + ], + "properties": { + "status": {"const": true}, + "openproject_redirect_uri": {"pattern": "^http:\/\/some-host.de\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, + "nextcloud_client_id": {"pattern": "[A-Za-z0-9]+"}, + "nextcloud_client_secret": {"pattern": "[A-Za-z0-9]+"}, + "openproject_user_app_password": {"pattern": "[A-Za-z0-9]+"} + }, + "not": { + "required": ["nextcloud_client_id","openproject_redirect_uri"] + } + } + """ + And user "OpenProject" should be the subadmin of the group "OpenProject" + And user "OpenProject" should be the subadmin of the group "OpenProjectNoAutomaticProjectFolders" + And groupfolder "OpenProject" should be assigned to the group "OpenProject" with all permissions + And groupfolder "OpenProject" should have advance permissions enabled + And groupfolder "OpenProject" should be managed by the user "OpenProject" + + + Scenario: try to setup and update twice with team folder + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": true, + "setup_app_password": true + } + } + """ + When the administrator sends a POST request to the "setup" endpoint with this data: + """ + { + "values" : { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "default_enable_navigation": false, + "default_enable_unified_search": false, + "setup_project_folder": true, + "setup_app_password": true + } + } + """ + Then the HTTP status code should be "409" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "The user \"OpenProject\" already exists"} + } + } + """ + When the administrator sends a PATCH request to the "setup" endpoint with this data: + """ + { + "values" : { + "setup_project_folder": true + } + } + """ + Then the HTTP status code should be "409" + And the data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": {"const": "The user \"OpenProject\" already exists"} + } + } + """ diff --git a/tests/acceptance/features/api/setup.feature b/tests/acceptance/features/api/setup.feature deleted file mode 100644 index 8c3137240..000000000 --- a/tests/acceptance/features/api/setup.feature +++ /dev/null @@ -1,872 +0,0 @@ -# SPDX-FileCopyrightText: 2022-2024 Jankari Tech Pvt. Ltd. -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: setup the integration through an API - - Scenario: valid setup without team 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 Outline: setup with invalid data - When the administrator sends a POST request to the "setup" endpoint with this data: - """ - { - "values" : { - "openproject_instance_url": , - "openproject_client_id": , - "openproject_client_secret": , - "default_enable_navigation": , - "default_enable_unified_search": , - "setup_project_folder": , - "setup_app_password": - } - } - """ - 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 data$"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | instance_url | openproject_client_id | openproject_client_secret | enable_navigation | enable_unified_search | setup_project_folder | setup_app_password | - | null | null | null | null | null | null | null | - | null | "id" | "secret" | false | false | false | false | - | "http://some-host.de" | null | "secret" | false | false | false | false | - | "http://some-host.de" | "id" | null | false | false | false | false | - | "http://some-host.de" | "id" | "secret" | null | false | false | false | - | "http://some-host.de" | "id" | "secret" | true | null | "" | "" | - | "" | "" | "" | "" | "" | false | false | - | "" | "id" | "secret" | false | false | false | false | - | "http://some-host.de" | "" | "secret" | false | false | false | false | - | "http://some-host.de" | "id" | "" | false | false | false | false | - | "http://some-host.de" | "id" | "secret" | "" | false | false | false | - | "http://some-host.de" | "id" | "secret" | true | "" | false | false | - | "ftp://somehost.de" | "the-id" | "secret" | true | false | "a string" | "a string" | - | "http://somehost.de" | false | "secret" | true | false | false | false | - | "http://somehost.de" | "id" | false | true | false | false | false | - | "http://somehost.de" | "the-id" | "secret" | "a string" | false | false | false | - | "http://somehost.de" | "the-id" | "secret" | false | "a string" | false | false | - - - Scenario: setup with invalid keys - When the administrator sends a POST request to the "setup" endpoint with this data: - """ - { - "values" : { - "instance_url": "http://openproject.de", - "client_id": "the-client" - } - } - """ - 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 key"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - - - Scenario Outline: setup with missing keys - When the administrator sends a POST request to the "setup" endpoint with this data: - """ - { - "values" : - } - """ - 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 key"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | values | - | {"openproject_client_id": "the-client-id", "openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "default_enable_unified_search": false} | - | {"openproject_instance_url": "http://some-host.de","openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "default_enable_unified_search": false, "setup_project_folder": false, "setup_app_password": false } | - | {"openproject_instance_url": "http://some-host.de", "openproject_client_id": "the-client-id", "default_enable_navigation": false, "default_enable_unified_search": false, "setup_project_folder": false, "setup_app_password": false} | - | {"openproject_instance_url": "http://some-host.de", "openproject_client_id": "the-client-id", "openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "setup_project_folder": false , "setup_app_password": false} | - - - Scenario Outline: setup with data that is not even valid JSON - When the administrator sends a POST request to the "setup" endpoint with this data: - """ - - """ - Then the HTTP status code should be "400" or "500" - Examples: - | data | - | "{}" | - | {"values": {"openproject_instance_url": "http://some-host.de","openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "default_enable_unified_search": false,}} | - | {"values": {"openproject_instance_url": "http://some-host.de","openproject_client_secret": "the-client-secret", "default_enable_navigation": false, "default_enable_unified_search": false} | - | {"values": | - | "values" | - | "" | - - Scenario: non-admin user tries to create the setup without project folder - Given user "Carol" has been created - When the user "Carol" 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 "403" - And the data of the response should match - """" - { - "type": "object", - "not": { - "required": [ - "nextcloud_oauth_client_name", - "openproject_redirect_uri", - "nextcloud_client_id", - "nextcloud_client_secret" - ] - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - - Scenario Outline: valid update - When the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values": { - "": - } - } - """ - 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" - ], - "properties": { - "nextcloud_oauth_client_name": {"type": "string", "pattern": "^OpenProject client$"}, - "openproject_redirect_uri": {"type": "string", "pattern": "^http:\/\/.*\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, - "nextcloud_client_id": {"type": "string", "pattern": "[A-Za-z0-9]+"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | key | value | - | openproject_instance_url | "http://some-host.de" | - | openproject_client_id | "client-value" | - | openproject_client_secret | "secret-value" | - | default_enable_navigation | false | - | default_enable_unified_search | true | - - - Scenario Outline: valid update of multiple values at once - When the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values": { - "": , - "": - } - } - """ - 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" - ], - "properties": { - "nextcloud_oauth_client_name": {"type": "string", "pattern": "^OpenProject client$"}, - "openproject_redirect_uri": {"type": "string", "pattern": "^http:\/\/.*\/oauth_clients\/[A-Za-z0-9]+\/callback$"}, - "nextcloud_client_id": {"type": "string", "pattern": "[A-Za-z0-9]+"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | key1 | value1 | key2 | value2 | - | openproject_instance_url | "http://some-host.de" | openproject_client_id | "client-value" | - | openproject_client_secret | "secret-value" | openproject_client_id | "client-value" | - | openproject_client_secret | "secret-value" | default_enable_navigation | false | - | default_enable_navigation | false | default_enable_unified_search | false | - - - Scenario Outline: update one value with invalid data - When the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values": { - "": - } - } - """ - 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": "^$"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | key | value | error-message | - | openproject_instance_url | null | invalid data | - | openproject_instance_url | "" | invalid data | - | openproject_instance_url | false | invalid data | - | openproject_client_id | null | invalid data | - | openproject_client_id | "" | invalid data | - | openproject_client_id | false | invalid data | - | openproject_client_secret | null | invalid data | - | openproject_client_secret | "" | invalid data | - | openproject_client_secret | false | invalid data | - | default_enable_navigation | null | invalid data | - | default_enable_navigation | "" | invalid data | - | default_enable_navigation | "string" | invalid data | - | default_enable_unified_search | null | invalid data | - | default_enable_unified_search | "" | invalid data | - | default_enable_unified_search | "string" | invalid data | - | instance_url | "http://op.de" | invalid key | - - - Scenario Outline: update of multiple values where at least one has invalid data - When the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values": { - "": , - "": - } - } - """ - 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 data$"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | key1 | value1 | key2 | value2 | - | openproject_instance_url | "http://some-host.de" | openproject_client_id | null | - | openproject_instance_url | "ftp://some-host.de" | openproject_client_id | "some id" | - | openproject_client_secret | "" | openproject_client_id | "client-value" | - | openproject_client_secret | "secret" | openproject_client_id | false | - | openproject_client_secret | "secret-value" | default_enable_navigation | "string" | - | default_enable_navigation | null | default_enable_unified_search | false | - - - Scenario Outline: with data that is not even valid JSON - When the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - - """ - Then the HTTP status code should be "400" or "500" - Examples: - | data | - | { "values": { "openproject_instance_url": "http://some-host.de"} }} | - | { "values": { "openproject_instance_url": "http://some-host.de"} | - | "values": { "openproject_instance_url": "http://some-host.de"} } | - | { "values": | - | "{}" | - | "" | - - - Scenario: non-admin tries to update the setup - Given user "Carol" has been created - When the user "Carol" sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values": { - "openproject_instance_url": "http://some-host.de" - } - } - """ - Then the HTTP status code should be "403" - And the data of the response should match - """" - { - "type": "object", - "not": { - "required": [ - "nextcloud_oauth_client_name", - "openproject_redirect_uri", - "nextcloud_client_id" - ] - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - - - Scenario: Reset the integration - When the administrator sends a DELETE request to the "setup" endpoint - Then the HTTP status code should be "200" - And the data of the response should match - """" - { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": {"type": "boolean", "enum": [ true ]} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - - - Scenario: Trying to reset the integration as non-admin - Given user "Carol" has been created - When the user "Carol" sends a DELETE request to the "setup" endpoint - Then the HTTP status code should be "403" - - Scenario Outline: Trying to setup whole integration, without project folder/user app password and vice-versa - 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": , - "setup_app_password": - } - } - """ - 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 data$"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - Examples: - | setup_project_folder | setup_app_password | - | true | false | - | false | true | - - - # this test wil not pass locally if your system already has a `OpenProject` user/group setup and 'OpenProjectNoAutomaticProjectFolders' group setup - Scenario: Set up whole integration with project folder and user app password - 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": true, - "setup_app_password": true - } - } - """ - 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", - "openproject_user_app_password" - ], - "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]+"}, - "openproject_user_app_password": {"type": "string", "pattern": "[A-Za-z0-9]+"} - }, - "not": { - "required": [ - "openproject_revocation_status" - ] - } - } - """ - And user "OpenProject" should be present in the server - And group "OpenProject" should be present in the server - And user "OpenProject" should be the subadmin of the group "OpenProject" - And group "OpenProjectNoAutomaticProjectFolders" should be present in the server - And user "OpenProject" should be the subadmin of the group "OpenProjectNoAutomaticProjectFolders" - And groupfolder "OpenProject" should be present in the server - And groupfolder "OpenProject" should be assigned to the group "OpenProject" with all permissions - And groupfolder "OpenProject" should have advance permissions enabled - And groupfolder "OpenProject" should be managed by the user "OpenProject" - # the next step is only for the tests, because that user has a random password - Given the administrator has changed the password of "OpenProject" to the default testing password - And user "OpenProject" should have a folder called "OpenProject" - # folders inside the OpenProject folder can only be deleted/renamed by the OpenProject user - And user "Carol" has been created - And user "Carol" has been added to the group "OpenProject" - And user "OpenProject" has created folder "/OpenProject/project-abc" - Then user "Carol" should have a folder called "OpenProject/project-abc" - When user "Carol" deletes folder "/OpenProject/project-abc" - Then the HTTP status code should be 500 - When user "Carol" renames folder "/OpenProject/project-abc" to "/OpenProject/project-123" - Then the HTTP status code should be 500 - When user "OpenProject" renames folder "/OpenProject/project-abc" to "/OpenProject/project-123" - Then the HTTP status code should be 201 - When user "OpenProject" deletes folder "/OpenProject/project-123" - Then the HTTP status code should be 204 - - # folders 2 levels down inside the OpenProject folder can be deleted by any user even if the parent is also called "OpenProject" - Given user "OpenProject" has created folder "/OpenProject/OpenProject/project-abc" - When user "Carol" renames folder "/OpenProject/OpenProject/project-abc" to "/OpenProject/OpenProject/project-123" - Then the HTTP status code should be 201 - When user "Carol" deletes folder "/OpenProject/OpenProject/project-123" - Then the HTTP status code should be 204 - - # a user, who is not in the OpenProject group can delete/rename items inside a folder that is called OpenProject - Given user "Brian" has been created - And user "Brian" has created folder "/OpenProject/project-abc" - When user "Brian" renames folder "/OpenProject/project-abc" to "/OpenProject/project-123" - Then the HTTP status code should be 201 - When user "Brian" deletes folder "/OpenProject/project-123" - Then the HTTP status code should be 204 - - # check deleting / disabling the OpenProject user/group - When the administrator deletes the user "OpenProject" - Then the HTTP status code should be 400 - And user "OpenProject" should be present in the server - When the administrator deletes the group "OpenProject" - Then the HTTP status code should be 400 - And group "OpenProject" should be present in the server - When the administrator disables the user "OpenProject" - Then the HTTP status code should be 400 - - # resending setup request will fail - 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": true, - "setup_app_password": true - } - } - """ - 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": "^The user \"OpenProject\" already exists$"} - } - } - """ - - # sending a PATCH request with setup_project_folder=true will also fail - When the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values" : { - "setup_project_folder": true - } - } - """ - 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": "^The user \"OpenProject\" already exists$"} - } - } - """ - - # we can make api request using the created app password for user "OpenProject" - When user "OpenProject" sends a "PROPFIND" request to "/remote.php/webdav" using current app password - Then the HTTP status code should be "207" - - # this is to provide test coverage for issues like this - # https://community.openproject.org/projects/nextcloud-integration/work_packages/49621 - When a new browser session for "Openproject" starts - # but other values can be updated by sending a PATCH request - # also we can replace old app password by sending PATCH request to get new user app password - And the administrator sends a PATCH request to the "setup" endpoint with this data: - """ - { - "values" : { - "default_enable_navigation": true, - "setup_app_password": true - } - } - """ - 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" - ], - "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]+"}, - "openproject_user_app_password": {"type": "string", "pattern": "[A-Za-z0-9]+"} - } - } - """ - And the newly generated app password should be different from the previous one - - # user "OpenProject" can make api request using the newly created app password - When user "OpenProject" sends a "PROPFIND" request to "/remote.php/webdav" using new app password - Then the HTTP status code should be "207" - - # user "OpenProject" cannot make api request using the old app password - When user "OpenProject" sends a "PROPFIND" request to "/remote.php/webdav" using old app password - Then the HTTP status code should be "401" - - - Scenario: check version of uploaded file inside a team folder - Given user "Carol" has been created - And user "Carol" has been added to the group "OpenProject" - And user "Carol" has created folder "/OpenProject/OpenProject/project-demo" - And user "Carol" got a direct-upload token for "/OpenProject/OpenProject/project-demo" - 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 | - Then the version folder of file "/OpenProject/OpenProject/project-demo/file.txt" for user "Carol" should contain "1" element - When user "Carol" deletes folder "/OpenProject/OpenProject/project-demo" - Then the HTTP status code should be 204 - - - Scenario: check version of uploaded file after an update inside a team folder - Given user "Carol" has been created - And user "Carol" has been added to the group "OpenProject" - And user "OpenProject" has created folder "/OpenProject/OpenProject/project-test" - And user "Carol" has uploaded file with content "0123456789" to "/OpenProject/OpenProject/project-test/file.txt" - And user "Carol" got a direct-upload token for "/OpenProject/OpenProject/project-test" - 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 | - | overwrite | true | - Then the HTTP status code should be "200" - And the version folder of file "/OpenProject/OpenProject/project-test/file.txt" for user "Carol" should contain "2" elements - When user "Carol" deletes folder "/OpenProject/OpenProject/project-test" - Then the HTTP status code should be 204 - - - Scenario: check OpenProjectNoAutomaticProjectFolders group after user is removed from OpenProject group (removed by group admin) - Given user "Carol" has been created - And user "Carol" has been added to the group "OpenProject" - When the group admin "OpenProject" removes the user "Carol" from group "OpenProject" - Then the HTTP status code should be 200 - And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" - And user "Carol" should not belong to group "OpenProject" - - - Scenario: user not in OpenProject group is removed from another group (removed by group admin) - Given group "grp1" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | grp1 | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - # group admin cannot remove a user from their last remaining group - When the group admin "OpenProject" removes the user "Carol" from group "grp1" - Then the HTTP status code should be 400 - And user "Carol" should belong to group "grp1" - And user "Carol" should not belong to group "OpenProjectNoAutomaticProjectFolders" - - - Scenario: user not in OpenProject group but has multiple group memberships is removed from one group (removed by group admin) - Given group "grp1" has been created - And group "grp2" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | grp2 | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - And user "OpenProject" has been assigned the role group admin of group "grp2" - When the group admin "OpenProject" removes the user "Carol" from group "grp1" - Then the HTTP status code should be 200 - And the following users should not belong to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProjectNoAutomaticProjectFolders | - And user "Carol" should belong to group "grp2" - - - Scenario: user in OpenProject and other groups (removed by group admin) - Given group "grp1" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProject | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - When the group admin "OpenProject" removes the user "Carol" from group "grp1" - Then the HTTP status code should be 200 - And the following users should not belong to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProjectNoAutomaticProjectFolders | - # A user cannot be removed from their last group - Given user "Carol" has been added to the group "grp1" - When the group admin "OpenProject" removes the user "Carol" from group "OpenProject" - Then the HTTP status code should be 200 - And user "Carol" should belong to group "grp1" - And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" - And user "Carol" should not belong to group "OpenProject" - - - Scenario: multiple user in OpenProject groups and only one gets removed (removed by group admin) - Given user "Alex" has been created - And user "Brian" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Alex | OpenProject | - | Brian | OpenProject | - | Carol | OpenProject | - When the group admin "OpenProject" removes the user "Carol" from group "OpenProject" - Then the HTTP status code should be 200 - And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" - And the following users should not belong to the following groups - | username | groupname | - | Alex | OpenProjectNoAutomaticProjectFolders | - | Brian | OpenProjectNoAutomaticProjectFolders | - | Carol | OpenProject | - - - Scenario: user is in multiple groups including OpenProject and is removed from another group (removed by group admin) - Given group "grp1" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | OpenProject | - | Carol | grp1 | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - When the group admin "OpenProject" removes the user "Carol" from group "grp1" - Then the HTTP status code should be 200 - And the following users should not belong to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProjectNoAutomaticProjectFolders | - And user "Carol" should belong to group "OpenProject" - - - Scenario: user not in OpenProject group is removed from another group (removed by admin) - Given group "grp1" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | grp1 | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - When the administrator removes the user "Carol" from group "grp1" - Then the HTTP status code should be 200 - And the following users should not belong to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProjectNoAutomaticProjectFolders | - - - Scenario: user not in OpenProject group but has multiple group memberships is removed from one group (removed by admin) - Given group "grp1" has been created - And group "grp2" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | grp2 | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - And user "OpenProject" has been assigned the role group admin of group "grp2" - When the administrator removes the user "Carol" from group "grp1" - Then the HTTP status code should be 200 - And the following users should not belong to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProjectNoAutomaticProjectFolders | - And user "Carol" should belong to group "grp2" - - - Scenario: user in OpenProject and other groups (removed by admin) - Given group "grp1" has been created - And user "Carol" has been created - And the following users have been added to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProject | - | OpenProject | grp1 | - And user "OpenProject" has been assigned the role group admin of group "grp1" - When the administrator removes the user "Carol" from group "grp1" - Then the HTTP status code should be 200 - And the following users should not belong to the following groups - | username | groupname | - | Carol | grp1 | - | Carol | OpenProjectNoAutomaticProjectFolders | - When the administrator removes the user "Carol" from group "OpenProject" - Then the HTTP status code should be 200 - And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" - And user "Carol" should not belong to group "OpenProject" diff --git a/tests/acceptance/features/api/teamFolder.feature b/tests/acceptance/features/api/teamFolder.feature new file mode 100644 index 000000000..a9fddc85b --- /dev/null +++ b/tests/acceptance/features/api/teamFolder.feature @@ -0,0 +1,213 @@ +# SPDX-FileCopyrightText: 2026 Jankari Tech Pvt. Ltd. +# SPDX-License-Identifier: AGPL-3.0-or-later + +@integration-setup +Feature: setup the integration through an API + + Background: + Given the administrator has set up the integration with the following settings: + """ + { + "values": { + "openproject_instance_url": "http://some-host.de", + "authorization_method": "oidc", + "sso_provider_type": "nextcloud_hub", + "targeted_audience_client_id": "openproject", + "setup_project_folder": true, + "setup_app_password": true, + "default_enable_navigation": false, + "default_enable_unified_search": false + } + } + """ + + + Scenario: check version of uploaded file inside a team folder + Given user "Carol" has been created + And user "Carol" has been added to the group "OpenProject" + And user "Carol" has created folder "/OpenProject/OpenProject/project-demo" + And user "Carol" got a direct-upload token for "/OpenProject/OpenProject/project-demo" + 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 | + Then the version folder of file "/OpenProject/OpenProject/project-demo/file.txt" for user "Carol" should contain "1" element + When user "Carol" deletes folder "/OpenProject/OpenProject/project-demo" + Then the HTTP status code should be 204 + + + Scenario: check version of uploaded file after an update inside a team folder + Given user "Carol" has been created + And user "Carol" has been added to the group "OpenProject" + And user "OpenProject" has created folder "/OpenProject/OpenProject/project-test" + And user "Carol" has uploaded file with content "0123456789" to "/OpenProject/OpenProject/project-test/file.txt" + And user "Carol" got a direct-upload token for "/OpenProject/OpenProject/project-test" + 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 | + | overwrite | true | + Then the HTTP status code should be "200" + And the version folder of file "/OpenProject/OpenProject/project-test/file.txt" for user "Carol" should contain "2" elements + When user "Carol" deletes folder "/OpenProject/OpenProject/project-test" + Then the HTTP status code should be 204 + + @expect-fail + Scenario: check OpenProjectNoAutomaticProjectFolders group after user is removed from OpenProject group (removed by group admin) + Given user "Carol" has been created + And user "Carol" has been added to the group "OpenProject" + When the group admin "OpenProject" removes the user "Carol" from group "OpenProject" + Then the HTTP status code should be 200 + And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" + And user "Carol" should not belong to group "OpenProject" + + @expect-fail + Scenario: user not in OpenProject group is removed from another group (removed by group admin) + Given group "grp1" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | grp1 | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + # group admin cannot remove a user from their last remaining group + When the group admin "OpenProject" removes the user "Carol" from group "grp1" + Then the HTTP status code should be 400 + And user "Carol" should belong to group "grp1" + And user "Carol" should not belong to group "OpenProjectNoAutomaticProjectFolders" + + @expect-fail + Scenario: user not in OpenProject group but has multiple group memberships is removed from one group (removed by group admin) + Given group "grp1" has been created + And group "grp2" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | grp2 | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + And user "OpenProject" has been assigned the role group admin of group "grp2" + When the group admin "OpenProject" removes the user "Carol" from group "grp1" + Then the HTTP status code should be 200 + And the following users should not belong to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProjectNoAutomaticProjectFolders | + And user "Carol" should belong to group "grp2" + + @expect-fail + Scenario: user in OpenProject and other groups (removed by group admin) + Given group "grp1" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProject | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + When the group admin "OpenProject" removes the user "Carol" from group "grp1" + Then the HTTP status code should be 200 + And the following users should not belong to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProjectNoAutomaticProjectFolders | + # A user cannot be removed from their last group + Given user "Carol" has been added to the group "grp1" + When the group admin "OpenProject" removes the user "Carol" from group "OpenProject" + Then the HTTP status code should be 200 + And user "Carol" should belong to group "grp1" + And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" + And user "Carol" should not belong to group "OpenProject" + + @expect-fail + Scenario: multiple user in OpenProject groups and only one gets removed (removed by group admin) + Given user "Alex" has been created + And user "Brian" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Alex | OpenProject | + | Brian | OpenProject | + | Carol | OpenProject | + When the group admin "OpenProject" removes the user "Carol" from group "OpenProject" + Then the HTTP status code should be 200 + And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" + And the following users should not belong to the following groups + | username | groupname | + | Alex | OpenProjectNoAutomaticProjectFolders | + | Brian | OpenProjectNoAutomaticProjectFolders | + | Carol | OpenProject | + + @expect-fail + Scenario: user is in multiple groups including OpenProject and is removed from another group (removed by group admin) + Given group "grp1" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | OpenProject | + | Carol | grp1 | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + When the group admin "OpenProject" removes the user "Carol" from group "grp1" + Then the HTTP status code should be 200 + And the following users should not belong to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProjectNoAutomaticProjectFolders | + And user "Carol" should belong to group "OpenProject" + + + Scenario: user not in OpenProject group is removed from another group (removed by admin) + Given group "grp1" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | grp1 | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + When the administrator removes the user "Carol" from group "grp1" + Then the HTTP status code should be 200 + And the following users should not belong to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProjectNoAutomaticProjectFolders | + + + Scenario: user not in OpenProject group but has multiple group memberships is removed from one group (removed by admin) + Given group "grp1" has been created + And group "grp2" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | grp2 | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + And user "OpenProject" has been assigned the role group admin of group "grp2" + When the administrator removes the user "Carol" from group "grp1" + Then the HTTP status code should be 200 + And the following users should not belong to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProjectNoAutomaticProjectFolders | + And user "Carol" should belong to group "grp2" + + + Scenario: user in OpenProject and other groups (removed by admin) + Given group "grp1" has been created + And user "Carol" has been created + And the following users have been added to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProject | + | OpenProject | grp1 | + And user "OpenProject" has been assigned the role group admin of group "grp1" + When the administrator removes the user "Carol" from group "grp1" + Then the HTTP status code should be 200 + And the following users should not belong to the following groups + | username | groupname | + | Carol | grp1 | + | Carol | OpenProjectNoAutomaticProjectFolders | + When the administrator removes the user "Carol" from group "OpenProject" + Then the HTTP status code should be 200 + And user "Carol" should belong to group "OpenProjectNoAutomaticProjectFolders" + And user "Carol" should not belong to group "OpenProject" diff --git a/tests/acceptance/features/bootstrap/FeatureContext.php b/tests/acceptance/features/bootstrap/FeatureContext.php index 76267f8a3..a197b542c 100644 --- a/tests/acceptance/features/bootstrap/FeatureContext.php +++ b/tests/acceptance/features/bootstrap/FeatureContext.php @@ -27,6 +27,10 @@ */ class FeatureContext implements Context { use JsonAssertions; + public const APP_ID = "integration_openproject"; + public const OPENPROJECT_USER = "OpenProject"; + public const OPENPROJECT_TEAM_FOLDER = "OpenProject"; + public const OPENPROJECT_GROUPS = ["OpenProject", "OpenProjectNoAutomaticProjectFolders"]; /** * list of users that were created on the local server during test runs @@ -132,6 +136,18 @@ public function __construct( $this->cookieJar = new CookieJar(); } + /** + * @param string $url + * + * @return string + */ + public function prefixJsonFormat(string $url): string { + if (str_contains($url, "?")) { + return $url . "&format=json"; + } + return $url . "?format=json"; + } + /** * @param string $user * @param array $userAttributes @@ -622,6 +638,39 @@ public function userEmptiedTrashbin(string $user):void { ); } + /** + * @Given the administrator has set up the integration with the following settings: + * + * @param PyStringNode $data + * @return void + */ + public function administratorHasSetupIntegrationWithFollowingSettings(PyStringNode $data): void { + $this->sendRequestsToAppEndpoint( + $this->adminUsername, $this->adminPassword, "POST", "/setup", $data + ); + $this->theHTTPStatusCodeShouldBe( + "200", + "Failed to set up the integration." + ); + + $response = json_decode($this->response->getBody()->getContents(), true); + Assert::assertArrayHasKey( + "status", + $response, + "Response does not contain 'status' property" + ); + Assert::assertTrue( + $response["status"] === true, + "Incomplete integration setup. Response: " . json_encode($response) + ); + + // save the created app password + if (isset($response["openproject_user_app_password"])) { + $this->createdAppPasswords[] = $response["openproject_user_app_password"]; + } + $this->setResponse(null); + } + /** * @When user :user gets the information of last created file */ @@ -680,12 +729,12 @@ public function userGetsTheInformationOfAllCreatedFiles(string $user, string $gr * * @param int|int[]|string|string[] $expectedStatusCode * @param string|null $message - * @param ResponseInterface $response + * @param ResponseInterface|null $response * * @return void */ public function theHTTPStatusCodeShouldBe( - $expectedStatusCode, ?string $message = "", $response = null + mixed $expectedStatusCode, ?string $message = "", ?ResponseInterface $response = null ): void { if ($response === null) { $response = $this->response; @@ -715,12 +764,35 @@ public function theHTTPStatusCodeShouldBe( } } + /** + * @param string $expectedStatus + * @param ResponseInterface|null $response + * + * @return void + */ + public function theOCSStatusShouldBe( + mixed $expectedStatus, + ?ResponseInterface $response = null + ): void { + if ($response === null) { + $response = $this->response; + } + $response->getBody()->rewind(); + $ocsData = json_decode($response->getBody()->getContents()); + Assert::assertNotNull($ocsData, 'The OCS response data is null'); + $actualStatus = $ocsData->ocs->meta->status; + Assert::assertEquals( + $expectedStatus, + $actualStatus, + "Expected OCS status '$expectedStatus' but got '$actualStatus'", + ); + } + /** * @Then the HTTP status code should be :code1 or :code2 * * @param string $code1 * @param string $code2 - * @param ResponseInterface $response * * @return void */ @@ -756,6 +828,7 @@ private function getJSONSchema(PyStringNode $schemaString) { public function theDataOfTheOCSResponseShouldMatch( PyStringNode $schemaString ): void { + $this->response->getBody()->rewind(); $responseAsJson = json_decode($this->response->getBody()->getContents()); $_responseAsJson = $responseAsJson->ocs->data; self::assertJsonDocumentMatchesSchema( @@ -773,6 +846,7 @@ public function theDataOfTheOCSResponseShouldMatch( public function theDataOfTheResponseShouldMatch( PyStringNode $schemaString ): void { + $this->response->getBody()->rewind(); $_responseAsJson = json_decode($this->response->getBody()->getContents()); self::assertJsonDocumentMatchesSchema( $_responseAsJson, @@ -953,10 +1027,11 @@ public function sendOCSRequest( $fullUrl = $this->getBaseUrl(); $fullUrl .= "ocs/v{$ocsApiVersion}.php" . $path; $headers['OCS-APIRequest'] = 'true'; - $headers['Accept'] = 'application/json'; - $headers['Content-Type'] = 'application/json'; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } return $this->sendHttpRequest( - $fullUrl, $user, $password, $method, $headers, $body + $this->prefixJsonFormat($fullUrl), $user, $password, $method, $headers, $body ); } @@ -981,6 +1056,11 @@ public function sendHttpRequest( ?array $options = [] ): ResponseInterface { if ($user !== null && $password !== null) { + // use the latest app password for OpenProject user. + if ($user === self::OPENPROJECT_USER && $password === $this->getRegularUserPassword()) { + $password = end($this->createdAppPasswords); + } + $options['auth'] = [$user, $password]; } $options['verify'] = false; @@ -1352,6 +1432,31 @@ public function verifyTableNodeRows(TableNode $table, array $requiredRows = [], } } + /** + * @param string $appId + * @param bool $enable + * + * @return void + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function enableOrDisableNextcloudApp(string $appId, bool $enable): void { + $method = $enable ? "POST" : "DELETE"; + $action = $enable ? "enable" : "disable"; + $response = $this->sendOCSRequest( + "/cloud/apps/{$appId}", + $method, + $this->getAdminUsername(), + ); + $this->theHTTPStatusCodeShouldBe(200, "Failed to {$action} app: " . $appId, $response); + $this->theOCSStatusShouldBe("ok", $response); + $response->getBody()->rewind(); + $body = json_decode($response->getBody()->getContents()); + Assert::assertTrue( + $body->ocs->meta->status === "ok", + "Failed to {$action} app: " . $appId . ". Response: " . json_encode($body) + ); + } + /** * This will run before EVERY scenario. * It will set the properties for this object. @@ -1375,6 +1480,20 @@ public function before(BeforeScenarioScope $scope):void { } } + /** + * @AfterScenario @integration-setup + * + * @return void + */ + public function teardownIntegrationSetup(): void { + $this->enableOrDisableNextcloudApp(self::APP_ID, true); + $this->sendRequestsToAppEndpoint( + $this->adminUsername, $this->adminPassword, "DELETE", "setup" + ); + $this->theHTTPStatusCodeShouldBe(200, "Failed to reset the integration setup"); + $this->setResponse(null); + } + /** * @AfterScenario * @@ -1404,8 +1523,12 @@ public function after():void { } // Clean up groups - foreach ($this->createdgroups as $groups) { - $this->theAdministratorDeletesTheGroup($groups); + foreach ($this->createdgroups as $group) { + if (!\in_array($group, self::OPENPROJECT_GROUPS, true)) { + $this->theAdministratorDeletesTheGroup($group); + $this->theHTTPStatusCodeShouldBe(200); + $this->setResponse(null); + } } $this->createdAppPasswords = []; } @@ -1433,6 +1556,12 @@ public function checkExpectedFailure(AfterScenarioScope $scope): void { $featurePath = "tests/" . explode("/tests/", $featurePath)[1]; $title = $scenario->getTitle(); $keyword = $scenario->getKeyword(); + if ($keyword === "Example") { + /** + * @psalm-suppress UndefinedInterfaceMethod + */ + $title = $scenario->getOutlineTitle(); + } $lineNumber = $scenario->getLine(); $scenarioLine = " - $keyword: $title ($featurePath:$lineNumber)"; diff --git a/tests/acceptance/features/bootstrap/GroupfoldersContext.php b/tests/acceptance/features/bootstrap/GroupfoldersContext.php index d9e177fc0..7fe0a8915 100644 --- a/tests/acceptance/features/bootstrap/GroupfoldersContext.php +++ b/tests/acceptance/features/bootstrap/GroupfoldersContext.php @@ -13,7 +13,6 @@ class GroupfoldersContext implements Context { /** - * * @var FeatureContext */ private $featureContext; @@ -21,14 +20,17 @@ class GroupfoldersContext implements Context { /** @var array */ private array $createdGroupFolders = []; + /** + * @return string + */ + public function getTeamFolderUrl(): string { + return $this->featureContext->getBaseUrl() . "index.php/apps/groupfolders/folders"; + } /** * @Given team folder :folderName has been created */ public function groupFolderHasBeenCreated(string $folderName): void { - $fullUrl = $this->featureContext->getBaseUrl() . - "index.php/apps/groupfolders/folders"; - $headers['OCS-APIRequest'] = 'true'; $options = [ 'multipart' => [ @@ -38,7 +40,7 @@ public function groupFolderHasBeenCreated(string $folderName): void { ] ]]; $response = $this->featureContext->sendHttpRequest( - $fullUrl, + $this->getTeamFolderUrl(), $this->featureContext->getAdminUsername(), $this->featureContext->getAdminPassword(), 'POST', @@ -58,8 +60,7 @@ public function groupFolderHasBeenCreated(string $folderName): void { */ public function groupHasBeenAddedToGroupFolder(string $group, string $groupfolder):void { $groupfolderId = $this->createdGroupFolders[$groupfolder]; - $fullUrl = $this->featureContext->getBaseUrl() . - "index.php/apps/groupfolders/folders/".$groupfolderId. "/groups"; + $fullUrl = $this->getTeamFolderUrl() . "/" . $groupfolderId . "/groups"; $headers['OCS-APIRequest'] = 'true'; $options = [ 'multipart' => [ @@ -94,7 +95,7 @@ public function groupfolderShouldBeAssignedToTheGroup(string $folderName, string $folder = $this->getGroupfolderByMountpoint($folderName); Assert::assertEquals( [ $folderName => 31 ], - $folder['groups'], + (array)$folder->groups, 'The group assignment of folder "' . $folderName . '" is not correct' . "\n" . print_r($folder, true) @@ -107,7 +108,7 @@ public function groupfolderShouldHaveAdvancePermissionsEnabled(string $folderNam $folder = $this->getGroupfolderByMountpoint($folderName); Assert::assertEquals( 1, - $folder['acl'], + $folder->acl, 'Folder "' . $folderName . '" has no ACLs enabled' . "\n" . print_r($folder, true) @@ -119,16 +120,21 @@ public function groupfolderShouldHaveAdvancePermissionsEnabled(string $folderNam */ public function groupfolderShouldBeManagedByTheUser(string $folderName, string $user): void { $folder = $this->getGroupfolderByMountpoint($folderName); + $manage = ($folder->manage)[0]; Assert::assertEquals( - [ - [ - "type" => "user", - "id" => $user, - "displayname" => $user - ] - ], - $folder['manage'], - 'manager of folder "' . $folderName . '" is not set correctly' + "user", + $manage->type, + 'Folder manager misconfigured: type' + ); + Assert::assertEquals( + $user, + $manage->id, + 'Folder manager misconfigured: id' + ); + Assert::assertEquals( + $user, + $manage->displayname, + 'Folder manager misconfigured: displayname' ); } @@ -142,55 +148,87 @@ public function userShouldHaveAFolderCalled(string $user, string $folderName): v ); } - /** - * @param string $mountpoint - * @return array - */ - private function getGroupfolderByMountpoint(string $mountpoint): array { - $body = $this->getAllGroupfolders(); - foreach ($body['ocs']['data'] as $groupfolder) { - if ($groupfolder['mount_point'] === $mountpoint) { - return $groupfolder; - } - } - throw new \Exception('could not find "' . $mountpoint . '" in the list of groupfolders' . - "\n" . print_r($body, true) - ); - } /** * @return array * @throws GuzzleException */ private function getAllGroupfolders() { - $fullUrl = $this->featureContext->getBaseUrl() . - "index.php/apps/groupfolders/folders?format=json"; - - $headers['Content-Type'] = 'application/json'; $headers['OCS-APIRequest'] = 'true'; - $response = $this->featureContext->sendHttpRequest( - $fullUrl, + $this->featureContext->prefixJsonFormat($this->getTeamFolderUrl()), $this->featureContext->getAdminUsername(), $this->featureContext->getAdminPassword(), 'GET', $headers ); + $this->featureContext->theHTTPStatusCodeShouldBe(200, "Failed to list team folders.", $response); + $this->featureContext->theOCSStatusShouldBe("ok", $response); + $response->getBody()->rewind(); + $body = json_decode($response->getBody()->getContents()); + Assert::assertTrue( + $body->ocs->meta->status === "ok", + "Failed to list team folders. Response: " . json_encode($body) + ); + return $body->ocs->data; + } + + /** + * @param string $mountpoint + * @return object + */ + private function getGroupfolderByMountpoint(string $mountpoint): object { + $folders = $this->getAllGroupfolders(); + foreach ($folders as $groupfolder) { + if ($groupfolder->mount_point === $mountpoint) { + return $groupfolder; + } + } + throw new \Exception('could not find "' . $mountpoint . '" in the list of groupfolders' . + "\n" . print_r($folders, true) + ); + } - $body = json_decode($response->getBody()->getContents(), true); - Assert::assertNotNull($body, 'could not decode body'); - return $body; + /** + * @return int + */ + public function getTeamFolderId(string $teamFolder): int { + $folderId = 0; + $folders = $this->getAllGroupfolders(); + foreach ($folders as $folder) { + if ($folder->mount_point === $teamFolder) { + $folderId = $folder->id; + break; + } + } + return $folderId; } - private function adminDeletesGroupfolder(int $id): void { - $fullUrl = $this->featureContext->getBaseUrl() . - "index.php/apps/groupfolders/folders/" . $id; + /** + * @param int $folderId + * + * @return void + */ + private function deleteTeamFolder(int $folderId): void { + if (!$folderId) { + return; + } + + $fullUrl = $this->getTeamFolderUrl() . "/" . $folderId; $headers['OCS-APIRequest'] = 'true'; - $this->featureContext->sendHttpRequest( - $fullUrl, + $response = $this->featureContext->sendHttpRequest( + $this->featureContext->prefixJsonFormat($fullUrl), $this->featureContext->getAdminUsername(), $this->featureContext->getAdminPassword(), - 'DELETE', - $headers + "DELETE", + $headers, + ); + $this->featureContext->theHTTPStatusCodeShouldBe(200, "Failed to delete team folder", $response); + $this->featureContext->theOCSStatusShouldBe("ok", $response); + $response->getBody()->rewind(); + $body = json_decode($response->getBody()->getContents()); + Assert::assertTrue( + $body->ocs->meta->status === "ok", + "Failed to delete team folder. Response: " . json_encode($body) ); } @@ -214,6 +252,29 @@ public function before(BeforeScenarioScope $scope):void { } } + /** + * @AfterScenario @integration-setup + * + * @return void + */ + public function teardownOpenProjectTeamFolder(): void { + $this->featureContext->enableOrDisableNextcloudApp(FeatureContext::APP_ID, false); + $this->deleteTeamFolder($this->getTeamFolderId(FeatureContext::OPENPROJECT_TEAM_FOLDER)); + + $this->featureContext->theAdministratorDeletesTheUser(FeatureContext::OPENPROJECT_USER); + $this->featureContext->theHTTPStatusCodeShouldBe([200, 404]); + $this->featureContext->setResponse(null); + + foreach (FeatureContext::OPENPROJECT_GROUPS as $group) { + $this->featureContext->theAdministratorDeletesTheGroup($group); + // with v2.php, the group deletion may return 200 or 400 if the group does not exist + $this->featureContext->theHTTPStatusCodeShouldBe([200, 400]); + $this->featureContext->setResponse(null); + } + + $this->featureContext->enableOrDisableNextcloudApp(FeatureContext::APP_ID, true); + } + /** * @AfterScenario * @@ -222,7 +283,8 @@ public function before(BeforeScenarioScope $scope):void { */ public function after():void { foreach ($this->createdGroupFolders as $groupFolder) { - $this->adminDeletesGroupfolder((int)$groupFolder); + $this->deleteTeamFolder((int)$groupFolder); } + $this->createdGroupFolders = []; } } diff --git a/tests/lib/Controller/ConfigControllerTest.php b/tests/lib/Controller/ConfigControllerTest.php index 77eda6250..3c5f1b85b 100644 --- a/tests/lib/Controller/ConfigControllerTest.php +++ b/tests/lib/Controller/ConfigControllerTest.php @@ -44,6 +44,32 @@ protected function tearDown(): void { } } + /** + * @param MockObject $mock The mock object on which the method is expected to be called + * @param string $method The method name for which the expectations are set + * @param list, mixed}> $calls An array of [expected args, return value] tuples for each call + */ + private function expectMethodCalls( + $mock, + string $method, + array $calls, + ?bool $exactly = false + ): void { + if ($exactly) { + $expectedCallsCount = $this->exactly(count($calls)); + } else { + $expectedCallsCount = $this->any(); + } + $mock + ->expects($expectedCallsCount) + ->method($method) + ->willReturnCallback(function (...$args) use (&$calls) { + [$expectedArgs, $returnValue] = array_shift($calls); + $this->assertSame($expectedArgs, $args); + return $returnValue; + }); + } + /** * @param string $codeVerifier The string that should be used as code_verifier * @param string $clientSecret The string that should be used as openproject_client_secret @@ -54,31 +80,23 @@ private function getConfigMock( $codeVerifier, $clientSecret, $startingPage = '{ page: "files" }' ) { $configMock = $this->getMockBuilder(IConfig::class)->getMock(); - $configMock - ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_client_id'], - ['integration_openproject', 'openproject_client_secret'], - ['integration_openproject', 'openproject_instance_url'], - ['integration_openproject', 'openproject_client_id'], - ['integration_openproject', 'openproject_client_secret'], - )->willReturnOnConsecutiveCalls( - 'clientID', $clientSecret, 'http://openproject.org', 'clientID', 'clientSecret', - ); + + $this->expectMethodCalls($configMock, 'getAppValue', [ + [['integration_openproject', 'openproject_client_id', ''], 'clientID'], + [['integration_openproject', 'openproject_client_secret', ''], $clientSecret], + [['integration_openproject', 'openproject_instance_url', ''], 'http://openproject.org'], + [['integration_openproject', 'openproject_client_id', ''], 'clientID'], + [['integration_openproject', 'openproject_client_secret', ''], 'clientSecret'], + ]); + $configMock ->method('getUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_state'], - ['testUser', 'integration_openproject', 'code_verifier'], - ['testUser', 'integration_openproject', 'oauth_journey_starting_page'], - ['testUser', 'integration_openproject', 'refresh_token'], - ) - ->willReturnOnConsecutiveCalls( - 'randomString', - $codeVerifier, - $startingPage, - 'oAuthRefreshToken', - ); + ->willReturnMap([ + ['testUser', 'integration_openproject', 'oauth_state', '', 'randomString'], + ['testUser', 'integration_openproject', 'code_verifier', false, $codeVerifier], + ['testUser', 'integration_openproject', 'oauth_journey_starting_page', '', $startingPage], + ['testUser', 'integration_openproject', 'refresh_token', false, 'oAuthRefreshToken'], + ]); return $configMock; } @@ -165,13 +183,10 @@ public function testOauthRedirectSuccess():void { str_repeat("A", 128), str_repeat("S", 50)); $configMock ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'token', 'oAuthAccessToken'], - ['testUser', 'integration_openproject', 'refresh_token', 'oAuthRefreshToken'], - ['testUser', 'integration_openproject', 'user_id', 1], - ['testUser', 'integration_openproject', 'user_name', 'Tripathi Himal'], - ['testUser', 'integration_openproject', 'oauth_connection_result', 'success'], + ->with( + 'testUser', 'integration_openproject', 'oauth_connection_result', 'success' ); + $urlGeneratorMock = $this->getMockBuilder(IURLGenerator::class) ->disableOriginalConstructor() ->getMock(); @@ -184,12 +199,12 @@ public function testOauthRedirectSuccess():void { ->getMock(); $apiServiceMock - ->method('request') + ->method('initUserInfo') ->with( 'testUser', - 'users/me' + 'oAuthAccessToken' ) - ->willReturn(['lastName' => 'Himal', 'firstName' => 'Tripathi', 'id' => 1]); + ->willReturn(['user_name' => 'Tripathi Himal']); $apiServiceMock ->method('requestOAuthAccessToken') @@ -272,16 +287,11 @@ public function testOauthRedirectWrongState() { $configMock = $this->getConfigMock( str_repeat("A", 128), str_repeat("S", 50) ); - $configMock - ->expects($this->exactly(2)) - ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_connection_result', 'error'], - [ - 'testUser', 'integration_openproject', 'oauth_connection_error_message', - 'Error during OAuth exchanges' - ], - ); + + $this->expectMethodCalls($configMock, 'setUserValue', [ + [['testUser', 'integration_openproject', 'oauth_connection_result', 'error', null], null], + [['testUser', 'integration_openproject', 'oauth_connection_error_message', 'Error during OAuth exchanges', null], null], + ]); $constructArgs = $this->getConfigControllerConstructArgs([ 'config' => $configMock, @@ -320,27 +330,17 @@ public function testOauthRedirectCodeVerifier($codeVerifier, $valid) { $loggerMock->expects($this->never()) ->method('error'); // even the secret is valid, we get an error because the token request is not mocked - $configMock->expects($this->exactly(2)) - ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_connection_result', 'error'], - [ - 'testUser', 'integration_openproject', 'oauth_connection_error_message', - 'Error getting OAuth access token' - ], - ); + $this->expectMethodCalls($configMock, 'setUserValue', [ + [['testUser', 'integration_openproject', 'oauth_connection_result', 'error', null], null], + [['testUser', 'integration_openproject', 'oauth_connection_error_message', 'Error getting OAuth access token', null], null], + ], true); } else { $loggerMock->expects($this->once()) ->method('error'); - $configMock->expects($this->exactly(2)) - ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_connection_result', 'error'], - [ - 'testUser', 'integration_openproject', 'oauth_connection_error_message', - 'Error during OAuth exchanges' - ], - ); + $this->expectMethodCalls($configMock, 'setUserValue', [ + [['testUser', 'integration_openproject', 'oauth_connection_result', 'error', null], null], + [['testUser', 'integration_openproject', 'oauth_connection_error_message', 'Error during OAuth exchanges', null], null], + ], true); } $constructArgs = $this->getConfigControllerConstructArgs([ @@ -383,25 +383,19 @@ public function testOauthRedirectSecret($clientSecret, $valid) { // even the secret is valid, we get an error because the token request is not mocked $configMock->expects($this->exactly(2)) ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_connection_result', 'error'], - [ - 'testUser', 'integration_openproject', 'oauth_connection_error_message', - 'Error getting OAuth access token' - ], - ); + ->willReturnMap([ + ['testUser', 'integration_openproject', 'oauth_connection_result', 'error', null, null], + ['testUser', 'integration_openproject', 'oauth_connection_error_message', 'Error getting OAuth access token', null, null], + ]); } else { $loggerMock->expects($this->once()) ->method('error'); $configMock->expects($this->exactly(2)) ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_connection_result', 'error'], - [ - 'testUser', 'integration_openproject', 'oauth_connection_error_message', - 'Error during OAuth exchanges' - ], - ); + ->willReturnMap([ + ['testUser', 'integration_openproject', 'oauth_connection_result', 'error', null, null], + ['testUser', 'integration_openproject', 'oauth_connection_error_message', 'Error during OAuth exchanges', null, null], + ]); } $constructArgs = $this->getConfigControllerConstructArgs([ @@ -462,12 +456,10 @@ public function testOauthNoAccessTokenInResponse($oauthResponse, $expectedErrorM $configMock = $this->getConfigMock( str_repeat("A", 128), str_repeat("S", 50) ); - $configMock->expects($this->exactly(2)) - ->method('setUserValue') - ->withConsecutive( - ['testUser', 'integration_openproject', 'oauth_connection_result', 'error'], - ['testUser', 'integration_openproject', 'oauth_connection_error_message', $expectedErrorMessage], - ); + $this->expectMethodCalls($configMock, 'setUserValue', [ + [['testUser', 'integration_openproject', 'oauth_connection_result', 'error', null], null], + [['testUser', 'integration_openproject', 'oauth_connection_error_message', $expectedErrorMessage, null], null], + ], true); $constructArgs = $this->getConfigControllerConstructArgs([ 'config' => $configMock, @@ -516,14 +508,13 @@ public function testSetAdminConfigForDifferentAdminConfigStatusForOauth2($credsT $configMock = $this->getMockBuilder(IConfig::class)->getMock(); $configMock - ->expects($this->exactly(4)) ->method('setAppValue') - ->withConsecutive( + ->willReturnMap([ ['integration_openproject', 'authorization_method', $credsToUpdate['authorization_method']], ['integration_openproject', 'openproject_client_id', $credsToUpdate['openproject_client_id']], ['integration_openproject', 'openproject_client_secret', $credsToUpdate['openproject_client_secret']], ['integration_openproject', 'openproject_instance_url', $credsToUpdate['openproject_instance_url']] - ); + ]); $configMock ->method('getAppValue') ->willReturnMap([ @@ -612,15 +603,14 @@ public function testSetAdminConfigForDifferentAdminConfigStatusForOIDC($credsToU $userManager = \OC::$server->get(IUserManager::class); $configMock = $this->getMockBuilder(IConfig::class)->getMock(); - $configMock - ->expects($this->exactly(4)) - ->method('setAppValue') - ->withConsecutive( - ['integration_openproject', 'authorization_method', $credsToUpdate['authorization_method']], - ['integration_openproject', 'oidc_provider', $credsToUpdate['oidc_provider']], - ['integration_openproject', 'targeted_audience_client_id', $credsToUpdate['targeted_audience_client_id']], - ['integration_openproject', 'openproject_instance_url', $credsToUpdate['openproject_instance_url']] - ); + + $this->expectMethodCalls($configMock, 'setAppValue', [ + [['integration_openproject', 'authorization_method', $credsToUpdate['authorization_method']], null], + [['integration_openproject', 'oidc_provider', $credsToUpdate['oidc_provider']], null], + [['integration_openproject', 'targeted_audience_client_id', $credsToUpdate['targeted_audience_client_id']], null], + [['integration_openproject', 'openproject_instance_url', $credsToUpdate['openproject_instance_url']], null], + ], true); + $configMock ->method('getAppValue') ->willReturnMap([ @@ -631,6 +621,8 @@ public function testSetAdminConfigForDifferentAdminConfigStatusForOIDC($credsToU [Application::APP_ID, 'targeted_audience_client_id', '', $credsToUpdate['targeted_audience_client_id']], [Application::APP_ID, 'oPOAuthTokenRevokeStatus', '', ''], ]); + + $apiService = $this->getMockBuilder(OpenProjectAPIService::class) ->disableOriginalConstructor() ->getMock(); @@ -822,26 +814,22 @@ public function testSetAdminConfigClearUserDataChangeNCOauthClient( $deleteCalls = []; if ($deleteUserValues === true) { $configMock - ->expects($this->exactly(12)) // 5 times for each user + ->expects($this->exactly(12)) ->method('deleteUserValue') - ->willReturnCallback(function ($uid, $app, $key) use (&$deleteCalls) { - $deleteCalls[] = [$uid, $app, $key]; - }); - - $expectedCalls = [ - ['admin', Application::APP_ID, 'token'], - ['admin', Application::APP_ID, 'login'], - ['admin', Application::APP_ID, 'user_id'], - ['admin', Application::APP_ID, 'user_name'], - ['admin', Application::APP_ID, 'refresh_token'], - ['admin', Application::APP_ID, 'token_expires_at'], - [$testUser, Application::APP_ID, 'token'], - [$testUser, Application::APP_ID, 'login'], - [$testUser, Application::APP_ID, 'user_id'], - [$testUser, Application::APP_ID, 'user_name'], - [$testUser, Application::APP_ID, 'refresh_token'], - [$testUser, Application::APP_ID, 'token_expires_at'], - ]; + ->willReturnMap([ + ['admin', Application::APP_ID, 'token', null], + ['admin', Application::APP_ID, 'login', null], + ['admin', Application::APP_ID, 'user_id', null], + ['admin', Application::APP_ID, 'user_name', null], + ['admin', Application::APP_ID, 'refresh_token', null], + ['admin', Application::APP_ID, 'token_expires_at', null], + [$testUser, Application::APP_ID, 'token', null], + [$testUser, Application::APP_ID, 'login', null], + [$testUser, Application::APP_ID, 'user_id', null], + [$testUser, Application::APP_ID, 'user_name', null], + [$testUser, Application::APP_ID, 'refresh_token', null], + [$testUser, Application::APP_ID, 'token_expires_at', null], + ]); } else { $configMock ->expects($this->never()) @@ -999,22 +987,16 @@ public function testSetAdminConfigForOPOAuthTokenRevoke(array $oldConfig, array $oauthSettingsControllerMock = $this->createMock('OCA\OAuth2\Controller\SettingsController'); if ($mode === "reset") { - $configMock - ->expects($this->exactly(3)) - ->method('deleteAppValue') - ->withConsecutive( - ['integration_openproject', 'nc_oauth_client_id'], - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ); + $this->expectMethodCalls($configMock, 'deleteAppValue', [ + [[Application::APP_ID, 'nc_oauth_client_id'], null], + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus'], null], + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus'], null], + ], true); } else { - $configMock - ->expects($this->exactly(2)) - ->method('deleteAppValue') - ->withConsecutive( - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ); + $this->expectMethodCalls($configMock, 'deleteAppValue', [ + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus'], null], + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus'], null], + ], true); } $configMock @@ -1027,17 +1009,19 @@ public function testSetAdminConfigForOPOAuthTokenRevoke(array $oldConfig, array [Application::APP_ID, 'nc_oauth_client_id', '', $oldAdminConfig['nc_oauth_client_id']], [Application::APP_ID, 'oPOAuthTokenRevokeStatus', '', ''], ]); + $configMock ->method('setAppValue') - ->withConsecutive( - ['integration_openproject', 'authorization_method', $newConfig['authorization_method']], - ['integration_openproject', 'openproject_client_id', $newConfig['openproject_client_id']], - ['integration_openproject', 'openproject_client_secret', $newConfig['openproject_client_secret']], - ['integration_openproject', 'openproject_instance_url', $newConfig['openproject_instance_url']], - ['integration_openproject', 'default_enable_navigation', $newConfig['default_enable_navigation']], - ['integration_openproject', 'default_enable_unified_search', $newConfig['default_enable_unified_search']], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', 'success'] - ); + ->willReturnMap([ + [Application::APP_ID, 'authorization_method', $newConfig['authorization_method'], null], + [Application::APP_ID, 'openproject_client_id', $newConfig['openproject_client_id'], null], + [Application::APP_ID, 'openproject_client_secret', $newConfig['openproject_client_secret'], null], + [Application::APP_ID, 'openproject_instance_url', $newConfig['openproject_instance_url'], null], + [Application::APP_ID, 'default_enable_navigation', $newConfig['default_enable_navigation'], null], + [Application::APP_ID, 'default_enable_unified_search', $newConfig['default_enable_unified_search'], null], + [Application::APP_ID, 'oPOAuthTokenRevokeStatus', 'success', null] + ]); + $configMock ->method('getUserValue') ->willReturnMap([ @@ -1048,28 +1032,29 @@ public function testSetAdminConfigForOPOAuthTokenRevoke(array $oldConfig, array $apiService ->expects($this->exactly(2)) ->method('revokeUserOAuthToken') - ->withConsecutive( - ['admin', $oldAdminConfig['openproject_instance_url'], $userTokens['admin'], $oldAdminConfig['openproject_client_id'], $oldAdminConfig['openproject_client_secret']], - [$testUser, $oldAdminConfig['openproject_instance_url'], $userTokens[$testUser], $oldAdminConfig['openproject_client_id'], $oldAdminConfig['openproject_client_secret']], - ); + ->willReturnMap([ + [$oldAdminConfig['openproject_instance_url'], $userTokens['admin'], $oldAdminConfig['openproject_client_id'], $oldAdminConfig['openproject_client_secret'], true], + [$oldAdminConfig['openproject_instance_url'], $userTokens[$testUser], $oldAdminConfig['openproject_client_id'], $oldAdminConfig['openproject_client_secret'], true], + ]); $configMock ->expects($this->exactly(12)) ->method("deleteUserValue") - ->withConsecutive( - ['admin', 'integration_openproject', 'token'], - ['admin', 'integration_openproject', 'login'], - ['admin', 'integration_openproject', 'user_id'], - ['admin', 'integration_openproject', 'user_name'], - ['admin', 'integration_openproject', 'refresh_token'], - ['admin', 'integration_openproject', 'token_expires_at'], - [$this->user1->getUID(), 'integration_openproject', 'token'], - [$this->user1->getUID(), 'integration_openproject', 'login'], - [$this->user1->getUID(), 'integration_openproject', 'user_id'], - [$this->user1->getUID(), 'integration_openproject', 'user_name'], - [$this->user1->getUID(), 'integration_openproject', 'refresh_token'], - [$this->user1->getUID(), 'integration_openproject', 'token_expires_at'], - ); + ->willReturnMap([ + ['admin', Application::APP_ID, 'token', null], + ['admin', Application::APP_ID, 'login', null], + ['admin', Application::APP_ID, 'user_id', null], + ['admin', Application::APP_ID, 'user_name', null], + ['admin', Application::APP_ID, 'refresh_token', null], + ['admin', Application::APP_ID, 'token_expires_at', null], + [$testUser, Application::APP_ID, 'token', null], + [$testUser, Application::APP_ID, 'login', null], + [$testUser, Application::APP_ID, 'user_id', null], + [$testUser, Application::APP_ID, 'user_name', null], + [$testUser, Application::APP_ID, 'refresh_token', null], + [$testUser, Application::APP_ID, 'token_expires_at', null], + ]); + $constructArgs = $this->getConfigControllerConstructArgs([ 'config' => $configMock, 'userManager' => $userManager, @@ -1136,45 +1121,32 @@ public function testOPOAuthTokenRevokeErrors($errorCode, $exception, $errMessage $oauthSettingsControllerMock = $this->createMock('OCA\OAuth2\Controller\SettingsController'); $loggerInterfaceMock = $this->createMock(LoggerInterface::class); + $this->expectMethodCalls($configMock, 'getAppValue', [ + [[Application::APP_ID, 'openproject_instance_url', ''], $oldAdminConfig['openproject_instance_url']], + [[Application::APP_ID, 'authorization_method', ''], $oldAdminConfig['authorization_method']], + [[Application::APP_ID, 'openproject_client_id', ''], $oldAdminConfig['openproject_client_id']], + [[Application::APP_ID, 'openproject_client_secret', ''], $oldAdminConfig['openproject_client_secret']], + [[Application::APP_ID, 'nc_oauth_client_id', ''], ''], + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus', ''], $errorCode], + [[Application::APP_ID, 'authorization_method', ''], Application::AUTH_METHOD_OAUTH], + [[Application::APP_ID, 'openproject_instance_url', ''], $newAdminConfig['openproject_instance_url']], + [[Application::APP_ID, 'fresh_project_folder_setup', ''], false], + [[Application::APP_ID, 'openproject_client_id', ''], $newAdminConfig['openproject_client_id']], + [[Application::APP_ID, 'openproject_client_secret', ''], $newAdminConfig['openproject_client_secret']], + [[Application::APP_ID, 'openproject_instance_url', ''], $newAdminConfig['openproject_instance_url']], + ]); + $configMock - ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_instance_url', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'openproject_client_id', ''], - ['integration_openproject', 'openproject_client_secret', ''], - ['integration_openproject', 'nc_oauth_client_id', ''], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], // for the last check - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'openproject_instance_url'], - ['integration_openproject', 'fresh_project_folder_setup'], - ['integration_openproject', 'openproject_client_id'], - ['integration_openproject', 'openproject_client_secret'], - ['integration_openproject', 'openproject_instance_url'], - ) - ->willReturnOnConsecutiveCalls( - $oldAdminConfig['openproject_instance_url'], - $oldAdminConfig['authorization_method'], - $oldAdminConfig['openproject_client_id'], - $oldAdminConfig['openproject_client_secret'], - '', - $errorCode, - Application::AUTH_METHOD_OAUTH, - $newAdminConfig['openproject_instance_url'], - false, - $newAdminConfig['openproject_client_id'], - $newAdminConfig['openproject_client_secret'], - $newAdminConfig['openproject_instance_url'], - ); - $configMock + ->expects($this->exactly(6)) ->method('setAppValue') - ->withConsecutive( - ['integration_openproject', 'authorization_method', $newAdminConfig['authorization_method']], - ['integration_openproject', 'openproject_client_id', $newAdminConfig['openproject_client_id']], - ['integration_openproject', 'openproject_client_secret', $newAdminConfig['openproject_client_secret']], - ['integration_openproject', 'openproject_instance_url', $newAdminConfig['openproject_instance_url']], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', $errorCode], - ); + ->willReturnMap([ + [Application::APP_ID, 'authorization_method', $newAdminConfig['authorization_method'], null], + [Application::APP_ID, 'openproject_client_id', $newAdminConfig['openproject_client_id'], null], + [Application::APP_ID, 'openproject_client_secret', $newAdminConfig['openproject_client_secret'], null], + [Application::APP_ID, 'openproject_instance_url', $newAdminConfig['openproject_instance_url'], null], + [Application::APP_ID, 'oPOAuthTokenRevokeStatus', $errorCode, null], + [Application::APP_ID, 'fresh_project_folder_setup', false, null], + ]); $configMock ->method('getUserValue') @@ -1184,42 +1156,39 @@ public function testOPOAuthTokenRevokeErrors($errorCode, $exception, $errMessage $loggerInterfaceMock ->method("error") - ->withConsecutive( - $errMessage, + ->with( + $errMessage[0], + $errMessage[1] ); $apiService ->expects($this->exactly(1)) ->method('revokeUserOAuthToken') - ->withConsecutive( - [ - 'admin', - $oldAdminConfig['openproject_instance_url'], - $userTokens['admin'], - $oldAdminConfig['openproject_client_id'], - $oldAdminConfig['openproject_client_secret'] - ], + ->with( + 'admin', + $oldAdminConfig['openproject_instance_url'], + $userTokens['admin'], + $oldAdminConfig['openproject_client_id'], + $oldAdminConfig['openproject_client_secret'] ) ->willThrowException($exception); $configMock ->expects($this->exactly(6)) ->method("deleteUserValue") - ->withConsecutive( - ['admin', 'integration_openproject', 'token'], - ['admin', 'integration_openproject', 'login'], - ['admin', 'integration_openproject', 'user_id'], - ['admin', 'integration_openproject', 'user_name'], - ['admin', 'integration_openproject', 'refresh_token'], - ['admin', 'integration_openproject', 'token_expires_at'], - ); - $configMock - ->expects($this->exactly(2)) - ->method('deleteAppValue') - ->withConsecutive( - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ); + ->willReturnMap([ + ['admin', Application::APP_ID, 'token', null], + ['admin', Application::APP_ID, 'login', null], + ['admin', Application::APP_ID, 'user_id', null], + ['admin', Application::APP_ID, 'user_name', null], + ['admin', Application::APP_ID, 'refresh_token', null], + ['admin', Application::APP_ID, 'token_expires_at', null], + ]); + + $this->expectMethodCalls($configMock, 'deleteAppValue', [ + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus'], null], + [[Application::APP_ID, 'oPOAuthTokenRevokeStatus'], null], + ], true); $constructArgs = $this->getConfigControllerConstructArgs([ 'config' => $configMock, @@ -1264,27 +1233,18 @@ public function testOPOAuthTokenRevokeDoesNotOccurIfNoOPOAuthClientHasChanged() $configMock ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_instance_url', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'openproject_client_id', ''], - ['integration_openproject', 'openproject_client_secret', ''], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', ''] - ) - ->willReturnOnConsecutiveCalls( - $oldAdminConfig['openproject_instance_url'], - $oldAdminConfig['authorization_method'], - $oldAdminConfig['openproject_client_id'], - $oldAdminConfig['openproject_client_secret'], - '' - ); + ->willReturnMap([ + [Application::APP_ID, 'openproject_instance_url', '', $oldAdminConfig['openproject_instance_url']], + [Application::APP_ID, 'authorization_method', '', $oldAdminConfig['authorization_method']], + [Application::APP_ID, 'openproject_client_id', '', $oldAdminConfig['openproject_client_id']], + [Application::APP_ID, 'openproject_client_secret', '', $oldAdminConfig['openproject_client_secret']], + [Application::APP_ID, 'oPOAuthTokenRevokeStatus', '', ''], + ]); + $configMock ->expects($this->exactly(2)) ->method('deleteAppValue') - ->withConsecutive( - ['integration_openproject', 'oPOAuthTokenRevokeStatus'], - ['integration_openproject', 'oPOAuthTokenRevokeStatus'] - ); + ->with(Application::APP_ID, 'oPOAuthTokenRevokeStatus'); $apiService ->expects($this->exactly(0)) @@ -1521,30 +1481,19 @@ public function testSetAdminConfigOIDCAuthSetting( ->getMock(); $configMock ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_instance_url', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'oidc_provider'], - ['integration_openproject', 'targeted_audience_client_id'], - ['integration_openproject', 'nc_oauth_client_id', ''], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'oidc_provider'], - ['integration_openproject', 'targeted_audience_client_id'], - ['integration_openproject', 'openproject_instance_url'], - ) - ->willReturnOnConsecutiveCalls( - $oldCreds['openproject_instance_url'], - $oldCreds['authorization_method'], - $oldCreds['oidc_provider'], - $oldCreds['targeted_audience_client_id'], - '123', - '', - Application::AUTH_METHOD_OAUTH, - $credsToUpdate['oidc_provider'], - $credsToUpdate['targeted_audience_client_id'], - $credsToUpdate['openproject_instance_url'] - ); + ->willReturnMap([ + ['integration_openproject', 'openproject_instance_url', '', $oldCreds['openproject_instance_url']], + ['integration_openproject', 'authorization_method', '', $oldCreds['authorization_method']], + ['integration_openproject', 'oidc_provider', '', $oldCreds['oidc_provider']], + ['integration_openproject', 'targeted_audience_client_id', '', $oldCreds['targeted_audience_client_id']], + ['integration_openproject', 'nc_oauth_client_id', '', '123'], + ['integration_openproject', 'oPOAuthTokenRevokeStatus', '', ''], + ['integration_openproject', 'authorization_method', '', Application::AUTH_METHOD_OIDC], + ['integration_openproject', 'oidc_provider', '', $credsToUpdate['oidc_provider']], + ['integration_openproject', 'targeted_audience_client_id', '', $credsToUpdate['targeted_audience_client_id']], + ['integration_openproject', 'openproject_instance_url', '', $credsToUpdate['openproject_instance_url']], + + ]); $apiService = $this->getMockBuilder(OpenProjectAPIService::class) ->disableOriginalConstructor() @@ -1617,33 +1566,22 @@ public function testSetAdminConfigForOAuth2AlreadyConfigured( $oauthSettingsControllerMock = $this->getMockBuilder(SettingsController::class) ->disableOriginalConstructor() ->getMock(); - $configMock - ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_instance_url', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'openproject_client_id'], - ['integration_openproject', 'openproject_client_secret'], - ['integration_openproject', 'nc_oauth_client_id', ''], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'openproject_client_id'], - ['integration_openproject', 'openproject_client_secret'], - ['integration_openproject', 'openproject_instance_url'], - ) - ->willReturnOnConsecutiveCalls( - $oldCreds['openproject_instance_url'], - $oldCreds['authorization_method'], - $oldCreds['openproject_client_id'], - $oldCreds['openproject_client_secret'], - '123', - '123', - '', - Application::AUTH_METHOD_OAUTH, - $credsToUpdate['openproject_client_id'], - $credsToUpdate['openproject_client_secret'], - $credsToUpdate['openproject_instance_url'] - ); + $this->expectMethodCalls($configMock, 'getAppValue', [ + [['integration_openproject', 'openproject_instance_url', ''], $oldCreds['openproject_instance_url']], + [['integration_openproject', 'authorization_method', ''], $oldCreds['authorization_method']], + [['integration_openproject', 'openproject_client_id', ''], $oldCreds['openproject_client_id']], + [['integration_openproject', 'openproject_client_secret', ''], $oldCreds['openproject_client_secret']], + [['integration_openproject', 'nc_oauth_client_id', ''], '123'], + [['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], '123'], + [['integration_openproject', 'authorization_method', ''], Application::AUTH_METHOD_OAUTH], + [['integration_openproject', 'openproject_client_id', ''], $credsToUpdate['openproject_client_id']], + [['integration_openproject', 'openproject_client_secret', ''], $credsToUpdate['openproject_client_secret']], + [['integration_openproject', 'openproject_instance_url', ''], $credsToUpdate['openproject_instance_url']], + ]); + $oauthSettingsControllerMock + ->expects($this->once()) + ->method('deleteClient') + ->with(123); $oauthSettingsControllerMock ->expects($this->once()) ->method('deleteClient') @@ -1651,20 +1589,20 @@ public function testSetAdminConfigForOAuth2AlreadyConfigured( $configMock ->expects($this->exactly(12)) ->method('deleteUserValue') - ->withConsecutive( - ['admin', 'integration_openproject', 'token'], - ['admin', 'integration_openproject', 'login'], - ['admin', 'integration_openproject', 'user_id'], - ['admin', 'integration_openproject', 'user_name'], - ['admin', 'integration_openproject', 'refresh_token'], - ['admin', 'integration_openproject', 'token_expires_at'], - [$this->user1->getUID(), 'integration_openproject', 'token'], - [$this->user1->getUID(), 'integration_openproject', 'login'], - [$this->user1->getUID(), 'integration_openproject', 'user_id'], - [$this->user1->getUID(), 'integration_openproject', 'user_name'], - [$this->user1->getUID(), 'integration_openproject', 'refresh_token'], - [$this->user1->getUID(), 'integration_openproject', 'token_expires_at'], - ); + ->willReturnMap([ + ['admin', 'integration_openproject', 'token', null], + ['admin', 'integration_openproject', 'login', null], + ['admin', 'integration_openproject', 'user_id', null], + ['admin', 'integration_openproject', 'user_name', null], + ['admin', 'integration_openproject', 'refresh_token', null], + ['admin', 'integration_openproject', 'token_expires_at', null], + [$this->user1->getUID(), 'integration_openproject', 'token', null], + [$this->user1->getUID(), 'integration_openproject', 'login', null], + [$this->user1->getUID(), 'integration_openproject', 'user_id', null], + [$this->user1->getUID(), 'integration_openproject', 'user_name', null], + [$this->user1->getUID(), 'integration_openproject', 'refresh_token', null], + [$this->user1->getUID(), 'integration_openproject', 'token_expires_at', null], + ]); $apiService = $this->getMockBuilder(OpenProjectAPIService::class) ->disableOriginalConstructor() @@ -1739,47 +1677,33 @@ public function testSetAdminConfigForOIDCAlreadyConfigured( $oauthSettingsControllerMock = $this->getMockBuilder(SettingsController::class) ->disableOriginalConstructor() ->getMock(); - $configMock - ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_instance_url', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'oidc_provider'], - ['integration_openproject', 'targeted_audience_client_id'], - ['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], - ['integration_openproject', 'authorization_method', ''], - ['integration_openproject', 'oidc_provider'], - ['integration_openproject', 'targeted_audience_client_id'], - ['integration_openproject', 'openproject_instance_url'], - ) - ->willReturnOnConsecutiveCalls( - $oldConfig['openproject_instance_url'], - $oldConfig['authorization_method'], - $oldConfig['oidc_provider'], - $oldConfig['targeted_audience_client_id'], - '', - $newConfig['authorization_method'], - $newConfig['oidc_provider'], - $newConfig['targeted_audience_client_id'], - $newConfig['openproject_instance_url'] - ); - $configMock - ->expects($this->exactly(12)) - ->method('deleteUserValue') - ->withConsecutive( - ['admin', 'integration_openproject', 'token'], - ['admin', 'integration_openproject', 'login'], - ['admin', 'integration_openproject', 'user_id'], - ['admin', 'integration_openproject', 'user_name'], - ['admin', 'integration_openproject', 'refresh_token'], - ['admin', 'integration_openproject', 'token_expires_at'], - [$this->user1->getUID(), 'integration_openproject', 'token'], - [$this->user1->getUID(), 'integration_openproject', 'login'], - [$this->user1->getUID(), 'integration_openproject', 'user_id'], - [$this->user1->getUID(), 'integration_openproject', 'user_name'], - [$this->user1->getUID(), 'integration_openproject', 'refresh_token'], - [$this->user1->getUID(), 'integration_openproject', 'token_expires_at'], - ); + + $this->expectMethodCalls($configMock, 'getAppValue', [ + [['integration_openproject', 'openproject_instance_url', ''], $oldConfig['openproject_instance_url']], + [['integration_openproject', 'authorization_method', ''], $oldConfig['authorization_method']], + [['integration_openproject', 'oidc_provider', ''], $oldConfig['oidc_provider']], + [['integration_openproject', 'targeted_audience_client_id', ''], $oldConfig['targeted_audience_client_id']], + [['integration_openproject', 'oPOAuthTokenRevokeStatus', ''], ''], + [['integration_openproject', 'authorization_method', ''], $newConfig['authorization_method']], + [['integration_openproject', 'oidc_provider', ''], $newConfig['oidc_provider']], + [['integration_openproject', 'targeted_audience_client_id', ''], $newConfig['targeted_audience_client_id']], + [['integration_openproject', 'openproject_instance_url', ''], $newConfig['openproject_instance_url']], + ]); + + $this->expectMethodCalls($configMock, 'deleteUserValue', [ + [['admin', 'integration_openproject', 'token'], null], + [['admin', 'integration_openproject', 'login'], null], + [['admin', 'integration_openproject', 'user_id'], null], + [['admin', 'integration_openproject', 'user_name'], null], + [['admin', 'integration_openproject', 'refresh_token'], null], + [['admin', 'integration_openproject', 'token_expires_at'], null], + [[$this->user1->getUID(), 'integration_openproject', 'token'], null], + [[$this->user1->getUID(), 'integration_openproject', 'login'], null], + [[$this->user1->getUID(), 'integration_openproject', 'user_id'], null], + [[$this->user1->getUID(), 'integration_openproject', 'user_name'], null], + [[$this->user1->getUID(), 'integration_openproject', 'refresh_token'], null], + [[$this->user1->getUID(), 'integration_openproject', 'token_expires_at'], null], + ], true); $apiService = $this->getMockBuilder(OpenProjectAPIService::class) ->disableOriginalConstructor() diff --git a/tests/lib/Controller/FilesControllerTest.php b/tests/lib/Controller/FilesControllerTest.php index fd8d4fe6b..096e1e4f9 100644 --- a/tests/lib/Controller/FilesControllerTest.php +++ b/tests/lib/Controller/FilesControllerTest.php @@ -188,20 +188,20 @@ public function testGetFileInfoFileExistingButCannotGetNameInContextOfOwner(): v public function testGetFilesInfoFourIdsRequestedOneExistsOneInTrashOneNotExisitingOneForbidden(): void { $folderMock = $this->getMockBuilder(Folder::class)->getMock(); $folderMock->method('getById') - ->withConsecutive([123], [759], [365], [956]) - ->willReturnOnConsecutiveCalls( + ->willReturnMap( [ - $this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png') - ], - [], - [] + [123, [$this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png')]], + [759, []], + [365, []], + [956, []] + ] ); $cachedMountFileInfoMock = $this->getMockBuilder( ICachedMountFileInfo::class )->getMock(); $cachedMountFileInfoMock->method('getInternalPath') - ->willReturnOnConsecutiveCalls( + ->willReturn( 'files/logo.png', 'files_trashbin/files/welcome.txt.d1648724302', '/anotherUser/files/logo.png' @@ -214,19 +214,20 @@ public function testGetFilesInfoFourIdsRequestedOneExistsOneInTrashOneNotExisiti $mountCacheMock = $this->getMockBuilder(IUserMountCache::class)->getMock(); $mountCacheMock->method('getMountsForFileId') - ->withConsecutive([123], [759], [365], [956]) - ->willReturnOnConsecutiveCalls( - [$cachedMountFileInfoMock], - [$cachedMountFileInfoMock], - [], // not found - [$cachedMountFileInfoMock], + ->willReturnMap( + [ + [123, null, [$cachedMountFileInfoMock]], + [759, null, [$cachedMountFileInfoMock]], + [365, null, []], + [956, null, [$cachedMountFileInfoMock]] + ] ); $filesController = $this->getFilesControllerMock( ['getDavPermissions'], $folderMock, $mountCacheMock ); $filesController->method('getDavPermissions') - ->willReturnOnConsecutiveCalls('RGDNVW', 'RGDNVW'); + ->willReturn('RGDNVW', 'RGDNVW'); $result = $filesController->getFilesInfo([123, 759, 365, 956]); assertSame( @@ -269,13 +270,12 @@ public function testGetFilesInfoOneIdRequestedFileExistsReturnsOneResult(): void public function testGetFilesInfoThreeIdsRequestedOneFileExistsReturnsOneResult(): void { $folderMock = $this->getMockBuilder(Folder::class)->getMock(); $folderMock->method('getById') - ->withConsecutive([123], [256], [365]) - ->willReturnOnConsecutiveCalls( + ->willReturnMap( [ - $this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png') - ], - [], - [] + [123, [$this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png')]], + [256, []], + [365, []] + ] ); $cachedMountFileInfoMock = $this->getMockBuilder( @@ -290,7 +290,7 @@ public function testGetFilesInfoThreeIdsRequestedOneFileExistsReturnsOneResult() $mountCacheMock = $this->getMockBuilder(IUserMountCache::class) ->getMock(); $mountCacheMock->method('getMountsForFileId') - ->willReturnOnConsecutiveCalls( + ->willReturn( [$cachedMountFileInfoMock], [], [] ); @@ -314,20 +314,18 @@ public function testGetFilesInfoThreeIdsRequestedOneFileExistsReturnsOneResult() public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsOneResult(): void { $folderMock = $this->getMockBuilder(Folder::class)->getMock(); $folderMock->method('getById') - ->withConsecutive([123], [365]) - ->willReturnOnConsecutiveCalls( + ->willReturnMap( [ - $this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png') - ], - [ - $this->getNodeMock('image/png', 365, 'file', '/testUser/files/inFolder/image.png'), + [123, [$this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png')]], + [365, [$this->getNodeMock('image/png', 365, 'file', '/testUser/files/inFolder/image.png')]] ] ); + $cachedMountFileInfoMock = $this->getMockBuilder( ICachedMountFileInfo::class )->getMock(); $cachedMountFileInfoMock->method('getInternalPath') - ->willReturnOnConsecutiveCalls( + ->willReturn( 'files/logo.png', 'files/inFolder/image.png', ); @@ -345,7 +343,7 @@ public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsOneResul ['getDavPermissions'], $folderMock, $mountCacheMock ); $filesController->method('getDavPermissions') - ->willReturnOnConsecutiveCalls('RGDNVW', 'RGDNVW'); + ->willReturn('RGDNVW'); $result = $filesController->getFilesInfo([123,365]); assertSame( [ @@ -360,13 +358,10 @@ public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsOneResul public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsMultipleResults(): void { $folderMock = $this->getMockBuilder(Folder::class)->getMock(); $folderMock->method('getById') - ->withConsecutive([123], [365]) - ->willReturnOnConsecutiveCalls( - [ - $this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png'), - ], + ->willReturnMap( [ - $this->getNodeMock('image/png', 365, 'file', '/testUser/files/inFolder/image.png'), + [123, [$this->getNodeMock('image/png', 123, 'file', '/testUser/files/logo.png')]], + [365, [$this->getNodeMock('image/png', 365, 'file', '/testUser/files/inFolder/image.png')]] ] ); @@ -374,7 +369,7 @@ public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsMultiple ICachedMountFileInfo::class )->getMock(); $cachedMountFileInfoMock->method('getInternalPath') - ->willReturnOnConsecutiveCalls( + ->willReturn( 'files/logo.png', 'files/inFolder/image.png', ); @@ -392,7 +387,7 @@ public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsMultiple ['getDavPermissions'], $folderMock, $mountCacheMock ); $filesController->method('getDavPermissions') - ->willReturnOnConsecutiveCalls('RGDNVW', 'RGDNVW'); + ->willReturn('RGDNVW'); $result = $filesController->getFilesInfo([123,365]); assertSame( @@ -408,31 +403,36 @@ public function testGetFilesInfoTwoIdsRequestedAllFilesExistsEachReturnsMultiple public function testGetFilesInfoTwoIdsRequestedEachReturnsOneFolder(): void { $folderMock = $this->getMockBuilder(Folder::class)->getMock(); $folderMock->method('getById') - ->withConsecutive([2], [3]) - ->willReturnOnConsecutiveCalls( + ->willReturnMap([ [ - $this->getNodeMock( - 'httpd/unix-directory', - 2, - 'dir', - '/testUser/files/myFolder/a-sub-folder' - ) + 2, + [ + $this->getNodeMock( + 'httpd/unix-directory', + 2, + 'dir', + '/testUser/files/myFolder/a-sub-folder' + ) + ] ], [ - $this->getNodeMock( - 'httpd/unix-directory', - 3, - 'dir', - '/testUser/files' - ) - ] - ); + 3, + [ + $this->getNodeMock( + 'httpd/unix-directory', + 3, + 'dir', + '/testUser/files' + ) + ] + ], + ]); $cachedMountFileInfoMock = $this->getMockBuilder( ICachedMountFileInfo::class )->getMock(); $cachedMountFileInfoMock->method('getInternalPath') - ->willReturnOnConsecutiveCalls( + ->willReturn( 'files/myFolder/a-sub-folder', 'files' ); @@ -450,7 +450,7 @@ public function testGetFilesInfoTwoIdsRequestedEachReturnsOneFolder(): void { ['getDavPermissions'], $folderMock, $mountCacheMock ); $filesController->method('getDavPermissions') - ->willReturnOnConsecutiveCalls('RGDNVCK', 'RGDNVCK'); + ->willReturn('RGDNVCK', 'RGDNVCK'); $result = $filesController->getFilesInfo([2,3]); assertSame( @@ -508,31 +508,32 @@ public function testGetFilesInfoInvalidRequest(): void { public function testGetFilesInfoSendStringIds(): void { $folderMock = $this->getMockBuilder(Folder::class)->getMock(); $folderMock->method('getById') - ->withConsecutive([2], [3]) - ->willReturnOnConsecutiveCalls( - [ - $this->getNodeMock( + ->willReturnMap([ + [ 2, + [$this->getNodeMock( 'httpd/unix-directory', 2, 'dir', '/testUser/files/myFolder/a-sub-folder' - ) + )] ], - [ - $this->getNodeMock( - 'httpd/unix-directory', - 3, - 'dir', - '/testUser/files/testFolder' - ) + [ 3, + [ + $this->getNodeMock( + 'httpd/unix-directory', + 3, + 'dir', + '/testUser/files/testFolder' + ) + ] ] - ); + ]); $cachedMountFileInfoMock = $this->getMockBuilder( ICachedMountFileInfo::class )->getMock(); $cachedMountFileInfoMock->method('getInternalPath') - ->willReturnOnConsecutiveCalls( + ->willReturn( 'files/myFolder/a-sub-folder', '' ); @@ -550,7 +551,7 @@ public function testGetFilesInfoSendStringIds(): void { ['getDavPermissions'], $folderMock, $mountCacheMock ); $filesController->method('getDavPermissions') - ->willReturnOnConsecutiveCalls('RGDNVCK', 'RGDNVCK'); + ->willReturn('RGDNVCK', 'RGDNVCK'); $result = $filesController->getFilesInfo(["2","3"]); assertSame( diff --git a/tests/lib/Controller/OpenProjectAPIControllerTest.php b/tests/lib/Controller/OpenProjectAPIControllerTest.php index 40a7bbde7..416a6409b 100644 --- a/tests/lib/Controller/OpenProjectAPIControllerTest.php +++ b/tests/lib/Controller/OpenProjectAPIControllerTest.php @@ -61,18 +61,20 @@ public function getConfigMock(string $authorizationMethod = '', $authToken = nul $configMock = $this->getMockBuilder(IConfig::class)->getMock(); $configMock ->method('getUserValue') - ->withConsecutive( - ['test','integration_openproject', 'token'], - ['test','integration_openproject', 'refresh_token'], - )->willReturnOnConsecutiveCalls($token, 'refreshToken'); + ->willReturnMap( + [ + ['test', 'integration_openproject', 'token', '', $token], + ['test', 'integration_openproject', 'refresh_token', '', 'refreshToken'] + ] + ); $opUrl = $opUrl ?? 'https://openproject.org'; $configMock ->method('getAppValue') - ->withConsecutive( - ['integration_openproject', 'openproject_instance_url'], - ['integration_openproject', 'authorization_method'], - )->willReturnOnConsecutiveCalls($opUrl, $authorizationMethod); + ->willReturnMap([ + ['integration_openproject', 'openproject_instance_url', '', $opUrl], + ['integration_openproject', 'authorization_method', '', $authorizationMethod], + ]); return $configMock; } diff --git a/tests/lib/Controller/OpenProjectControllerTest.php b/tests/lib/Controller/OpenProjectControllerTest.php index d77e2f582..89708baa5 100644 --- a/tests/lib/Controller/OpenProjectControllerTest.php +++ b/tests/lib/Controller/OpenProjectControllerTest.php @@ -295,20 +295,10 @@ public function testGetOpenProjectOauthURLWithStateAndPKCE(): void { $configMock ->expects($this->exactly(2)) ->method('setUserValue') - ->withConsecutive( - [ - 'test', - 'integration_openproject', - 'oauth_state', - $this->matchesRegularExpression('/[a-z0-9]{10}/') - ], - [ - 'test', - 'integration_openproject', - 'code_verifier', - $this->matchesRegularExpression('/[A-Za-z0-9\-._~]{128}/') - ], - ); + ->willReturnMap([ + ['test', 'integration_openproject', 'oauth_state', $this->matchesRegularExpression('/[a-z0-9]{10}/'), null, null], + ['test', 'integration_openproject', 'code_verifier', $this->matchesRegularExpression('/[A-Za-z0-9\-._~]{128}/'), null, null], + ]); $controller = $this->getOpenProjectControllerMock([ 'openProjectAPIService' => $service, diff --git a/tests/lib/Service/OpenProjectAPIServiceTest.php b/tests/lib/Service/OpenProjectAPIServiceTest.php index f35e6926f..cdc4eeab8 100644 --- a/tests/lib/Service/OpenProjectAPIServiceTest.php +++ b/tests/lib/Service/OpenProjectAPIServiceTest.php @@ -727,29 +727,18 @@ private function getOpenProjectAPIService( $clientConfigMock = $this->getMockBuilder(IConfig::class)->getMock(); $clientConfigMock ->method('getSystemValueBool') - ->withConsecutive( - ['allow_local_remote_servers', false], - ['installed', false], - ['allow_local_remote_servers', false], - ['allow_local_remote_servers', false], - ['installed', false], - ['allow_local_remote_servers', false], - ['allow_local_remote_servers', false], - ['installed', false], - ['allow_local_remote_servers', false] - ) - ->willReturnOnConsecutiveCalls( - true, - true, - true, - true, - true, - true, - true, - true, - true + ->willReturnMap([ + ['allow_local_remote_servers', false, true], + ['installed', false, true], + ['allow_local_remote_servers', false, true], + ['allow_local_remote_servers', false, true], + ['installed', false, true], + ['allow_local_remote_servers', false, true], + ['allow_local_remote_servers', false, true], + ['installed', false, true], + ['allow_local_remote_servers', false, true] + ] ); - $clientService = $this->getMockBuilder(IClientService::class)->getMock(); $clientService->method('newClient')->willReturn($this->createMock(Client::class)); @@ -964,9 +953,10 @@ public function testSearchWorkPackageNotLinkedToAStorage(array $response, array public function testSearchWorkPackageByFileIdOnlyFileId() { $service = $this->getOpenProjectAPIServiceMock(); $service->method('request') - ->withConsecutive( + ->willReturnMap([ [ - 'user', 'work_packages', + 'user', + 'work_packages', [ 'filters' => '[' . '{"file_link_origin_id":{"operator":"=","values":["123"]}},'. @@ -974,12 +964,12 @@ public function testSearchWorkPackageByFileIdOnlyFileId() { '{"operator":"=","values":["https%3A%2F%2Fnc.my-server.org"]}}'. ']', 'sortBy' => '[["updatedAt","desc"]]', - ] - ], - ) - ->willReturnOnConsecutiveCalls( - ["_embedded" => ["elements" => [['id' => 1], ['id' => 2], ['id' => 3]]]] + ], + 'GET', + ["_embedded" => ["elements" => [['id' => 1], ['id' => 2], ['id' => 3]]]] + ]] ); + $result = $service->searchWorkPackage('user', null, 123); $this->assertSame([['id' => 1], ['id' => 2], ['id' => 3]], $result); } @@ -2219,14 +2209,12 @@ public function testRequestConnectException( ])); $configMock ->method('getUserValue') - ->withConsecutive( - ['','integration_openproject', 'token'], - ['','integration_openproject', 'token_expires_at'], - ['','integration_openproject', 'refresh_token'], - ) - ->willReturnOnConsecutiveCalls( - '', - '', + ->willReturnMap( + [ + ['','integration_openproject', 'token', '', ''], + ['','integration_openproject', 'token_expires_at', '', ''], + ['','integration_openproject', 'refresh_token', '', ''], + ] ); $constructArgs = $this->getOpenProjectAPIServiceConstructArgs([ @@ -2269,13 +2257,11 @@ public function testRequestClientServerException( ])); $configMock ->method('getUserValue') - ->withConsecutive( - ['','integration_openproject', 'token'], - ['','integration_openproject', 'refresh_token'], - ) - ->willReturnOnConsecutiveCalls( - '', - '', + ->willReturnMap( + [ + ['','integration_openproject', 'token', '', ''], + ['','integration_openproject', 'refresh_token', '', ''], + ] ); $constructArgs = $this->getOpenProjectAPIServiceConstructArgs([ @@ -2558,9 +2544,10 @@ public function testIsSystemReadyForProjectFolderSetUp(): void { $userManagerMock = $this->getMockBuilder(IUserManager::class) ->getMock(); $userManagerMock + ->expects($this->exactly(2)) ->method('userExists') - ->withConsecutive([Application::OPEN_PROJECT_ENTITIES_NAME], [Application::OPEN_PROJECT_ENTITIES_NAME]) - ->willReturnOnConsecutiveCalls(false, false); + ->with(Application::OPEN_PROJECT_ENTITIES_NAME) + ->willReturn(false); $userManagerMock ->method('get') ->with(Application::OPEN_PROJECT_ENTITIES_NAME) @@ -2569,9 +2556,10 @@ public function testIsSystemReadyForProjectFolderSetUp(): void { $groupManagerMock = $this->getMockBuilder(IGroupManager::class) ->getMock(); $groupManagerMock + ->expects($this->exactly(1)) ->method('groupExists') - ->withConsecutive([Application::OPEN_PROJECT_ENTITIES_NAME], [Application::OPEN_PROJECT_ENTITIES_NAME]) - ->willReturnOnConsecutiveCalls(false, false); + ->with(Application::OPEN_PROJECT_ENTITIES_NAME) + ->willReturn(false); $appManagerMock = $this->getMockBuilder(IAppManager::class) ->getMock(); $appManagerMock @@ -2579,21 +2567,15 @@ public function testIsSystemReadyForProjectFolderSetUp(): void { ->with('groupfolders', $userMock) ->willReturn(true); $service = $this->getOpenProjectAPIServiceMock( - ['getGroupFolderManager'], + ['isOpenProjectGroupfolderCreated'], [ 'userManager' => $userManagerMock, 'groupManager' => $groupManagerMock, 'appManager' => $appManagerMock, ], ); - $folderManagerMock = $this->getFolderManagerMock('', false, [ 0 => [ - 'folder_id' => 123, - 'mount_point' => '', - 'permissions' => 31, - 'acl' => true - ]]); - $service->method('getGroupFolderManager') - ->willReturn($folderManagerMock); + $service->method('isOpenProjectGroupfolderCreated') + ->willReturn(false); $result = $service->isSystemReadyForProjectFolderSetUp(); $this->assertTrue($result); } @@ -2838,7 +2820,12 @@ public function testProjectFolderDeleteAppPassword(): void { $service->method('hasAppPassword')->willReturn(true); $tokenProviderMock->expects($this->exactly(2)) ->method('invalidateTokenById') - ->withConsecutive([Application::OPEN_PROJECT_ENTITIES_NAME, 4], [Application::OPEN_PROJECT_ENTITIES_NAME, 5]); + ->willReturnMap( + [ + [Application::OPEN_PROJECT_ENTITIES_NAME, 4, null], + [Application::OPEN_PROJECT_ENTITIES_NAME, 5, null], + ] + ); $service->deleteAppPassword(); } @@ -4451,11 +4438,12 @@ public function testIsAdminAuditConfigSetCorrectly( $iAppManagerMock = $this->getMockBuilder(IAppManager::class)->getMock(); $configMock ->method('getSystemValue') - ->withConsecutive( - ['loglevel'], - ['logfile_audit'], - ['log.condition'] - )->willReturnOnConsecutiveCalls($logLevel, $pathToAuditLog, $logCondition); + ->willReturnMap([ + ['loglevel', '', $logLevel], + ['logfile_audit', '', $pathToAuditLog], + ['log.condition', '', $logCondition] + ] + ); $userManagerMock = $this->getMockBuilder(IUserManager::class) ->getMock(); $iAppManagerMock->method('isInstalled')->with('admin_audit')