From 3b11030244c4f3272bb92d11324256d50d235281 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Mon, 29 Apr 2024 11:52:24 +0200 Subject: [PATCH 1/3] [#54355] Use authentication in FolderFilesFileIdsDeepQuery - https://community.openproject.org/work_packages/54355 - harmonized interface between nextcloud and one_drive - path key of nextcloud folders no longer end with trailing slash - added vcr cassettes for nextcloud - added shared test examples for both provider types --- .../peripherals/nextcloud_registry.rb | 2 +- .../peripherals/one_drive_registry.rb | 2 +- .../nextcloud/file_ids_query.rb | 2 +- .../folder_files_file_ids_deep_query.rb | 51 ++-- .../nextcloud/internal/propfind_query.rb | 217 ++++++++++-------- .../internal/propfind_query_legacy.rb | 141 ++++++++++++ .../folder_files_file_ids_deep_query.rb | 170 ++++++-------- .../one_drive/internal/drive_item_query.rb | 15 +- .../storage_interaction/one_drive/util.rb | 158 +++++++------ .../storage_parent_folder_extractor.rb | 36 +-- .../app/models/storages/storage_file_id.rb | 33 +++ .../file_links/copy_file_links_service.rb | 16 +- .../nextcloud/create_folder_command_spec.rb | 12 +- .../folder_files_file_ids_deep_query_spec.rb | 88 +++++++ .../one_drive/create_folder_command_spec.rb | 12 +- .../folder_files_file_ids_deep_query_spec.rb | 164 +++++-------- ...les_file_ids_deep_query_invalid_parent.yml | 95 ++++++++ ...iles_file_ids_deep_query_parent_folder.yml | 98 ++++++++ .../folder_files_file_ids_deep_query_root.yml | 98 ++++++++ .../create_folder_command_examples.rb | 8 +- ...lder_files_file_ids_deep_query_examples.rb | 68 ++++++ .../storages/copy_project_folders_job_spec.rb | 4 +- 22 files changed, 1045 insertions(+), 445 deletions(-) create mode 100644 modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query_legacy.rb create mode 100644 modules/storages/app/models/storages/storage_file_id.rb create mode 100644 modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query_spec.rb create mode 100644 modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_invalid_parent.yml create mode 100644 modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_parent_folder.yml create mode 100644 modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_root.yml create mode 100644 modules/storages/spec/support/shared_examples_for_adapters/folder_files_file_ids_deep_query_examples.rb diff --git a/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb b/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb index 7ce7ab03fb6..951367fe7c9 100644 --- a/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb +++ b/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb @@ -38,7 +38,7 @@ module Storages register(:file_info, StorageInteraction::Nextcloud::FileInfoQuery) register(:files_info, StorageInteraction::Nextcloud::FilesInfoQuery) register(:files, StorageInteraction::Nextcloud::FilesQuery) - register(:folder_files_file_ids_deep_query, StorageInteraction::Nextcloud::FolderFilesFileIdsDeepQuery) + register(:folder_files_file_ids_deep, StorageInteraction::Nextcloud::FolderFilesFileIdsDeepQuery) register(:propfind, StorageInteraction::Nextcloud::Internal::PropfindQuery) register(:group_users, StorageInteraction::Nextcloud::GroupUsersQuery) register(:upload_link, StorageInteraction::Nextcloud::UploadLinkQuery) diff --git a/modules/storages/app/common/storages/peripherals/one_drive_registry.rb b/modules/storages/app/common/storages/peripherals/one_drive_registry.rb index e362a39daa6..4616fccb326 100644 --- a/modules/storages/app/common/storages/peripherals/one_drive_registry.rb +++ b/modules/storages/app/common/storages/peripherals/one_drive_registry.rb @@ -38,7 +38,7 @@ module Storages register(:file_info, StorageInteraction::OneDrive::FileInfoQuery) register(:files_info, StorageInteraction::OneDrive::FilesInfoQuery) register(:open_file_link, StorageInteraction::OneDrive::OpenFileLinkQuery) - register(:folder_files_file_ids_deep_query, StorageInteraction::OneDrive::FolderFilesFileIdsDeepQuery) + register(:folder_files_file_ids_deep, StorageInteraction::OneDrive::FolderFilesFileIdsDeepQuery) register(:open_storage, StorageInteraction::OneDrive::OpenStorageQuery) register(:upload_link, StorageInteraction::OneDrive::UploadLinkQuery) end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb index 65eece537c0..0335b6b6126 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb @@ -29,7 +29,7 @@ module Storages::Peripherals::StorageInteraction::Nextcloud class FileIdsQuery def initialize(storage) - @query = ::Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery.new(storage) + @query = ::Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQueryLegacy.new(storage) end def self.call(storage:, path:) diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb index 21c2b7c901b..4116284b833 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb @@ -28,19 +28,44 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::Nextcloud - class FolderFilesFileIdsDeepQuery - def self.call(storage:, folder:) - ::Storages::Peripherals::Registry - .resolve("nextcloud.queries.propfind") - .call( - storage:, - depth: "infinity", - path: folder.path, - # nc:acl-list is only required to avoid https://community.openproject.org/wp/49628. See comment #4. - props: %w[oc:fileid nc:acl-list] - ).map do |obj| - obj.transform_values { |value| Storages::StorageFileInfo.from_id(value["fileid"]) } +module Storages + module Peripherals + module StorageInteraction + module Nextcloud + class FolderFilesFileIdsDeepQuery + def self.call(storage:, auth_strategy:, folder:) + new(storage).call(auth_strategy:, folder:) + end + + def initialize(storage) + @storage = storage + @propfind_query = Internal::PropfindQuery.new(storage) + end + + def call(auth_strategy:, folder:) + origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) + if origin_user_id.failure? + return origin_user_id + end + + Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| + # nc:acl-list is only required to avoid https://community.openproject.org/wp/49628. See comment #4. + @propfind_query.call(http:, + username: origin_user_id.result, + path: folder.path, + props: %w[oc:fileid nc:acl-list]) + .map do |obj| + obj.transform_values { |value| StorageFileId.new(id: value["fileid"]) } + end + end + end + + private + + def http_options + Util.webdav_request_with_depth("infinity") + end + end end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb index 876ab27f04f..445342bc46f 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb @@ -28,114 +28,131 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::Nextcloud::Internal - class PropfindQuery - UTIL = ::Storages::Peripherals::StorageInteraction::Nextcloud::Util +module Storages + module Peripherals + module StorageInteraction + module Nextcloud + module Internal + class PropfindQuery + # Only for information purposes currently. + # Probably a bit later we could validate `#call` parameters. + # + # DEPTH = %w[0 1 infinity].freeze + # POSSIBLE_PROPS = %w[ + # d:getlastmodified + # d:getetag + # d:getcontenttype + # d:resourcetype + # d:getcontentlength + # d:permissions + # d:size + # oc:id + # oc:fileid + # oc:favorite + # oc:comments-href + # oc:comments-count + # oc:comments-unread + # oc:owner-id + # oc:owner-display-name + # oc:share-types + # oc:checksums + # oc:size + # nc:has-preview + # nc:rich-workspace + # nc:contained-folder-count + # nc:contained-file-count + # nc:acl-list + # ].freeze - # Only for information purposes currently. - # Probably a bit later we could validate `#call` parameters. - # - # DEPTH = %w[0 1 infinity].freeze - # POSSIBLE_PROPS = %w[ - # d:getlastmodified - # d:getetag - # d:getcontenttype - # d:resourcetype - # d:getcontentlength - # d:permissions - # d:size - # oc:id - # oc:fileid - # oc:favorite - # oc:comments-href - # oc:comments-count - # oc:comments-unread - # oc:owner-id - # oc:owner-display-name - # oc:share-types - # oc:checksums - # oc:size - # nc:has-preview - # nc:rich-workspace - # nc:contained-folder-count - # nc:contained-file-count - # nc:acl-list - # ].freeze + def self.call(storage:, http:, username:, path:, props:) + new(storage).call(http:, username:, path:, props:) + end - def initialize(storage) - @uri = storage.uri - @username = storage.username - @password = storage.password - @group = storage.group - end + def initialize(storage) + @storage = storage + end - def self.call(storage:, depth:, path:, props:) - new(storage).call(depth:, path:, props:) - end + def call(http:, username:, path:, props:) + request_uri = Util.join_uri_path(base_uri, CGI.escapeURIComponent(username), Util.escape_path(path)) + response = http.request(:propfind, request_uri, xml: request_body(props)) - # rubocop:disable Metrics/AbcSize - def call(depth:, path:, props:) - body = Nokogiri::XML::Builder.new do |xml| - xml["d"].propfind( - "xmlns:d" => "DAV:", - "xmlns:oc" => "http://owncloud.org/ns", - "xmlns:nc" => "http://nextcloud.org/ns" - ) do - xml["d"].prop do - props.each do |prop| - namespace, property = prop.split(":") - xml[namespace].send(property) + handle_response(response, username) + end + + private + + def handle_response(response, username) + case response + in { status: 200..299 } + success_result(response, username) + in { status: 401 } + Util.failure(code: :unauthorized, + data: Util.error_data_from_response(caller: self.class, response:), + log_message: "Outbound request not authorized!") + in { status: 404 } + Util.failure(code: :not_found, + data: Util.error_data_from_response(caller: self.class, response:), + log_message: "Outbound request destination not found!") + in { status: 405 } + Util.failure(code: :not_allowed, + data: Util.error_data_from_response(caller: self.class, response:), + log_message: "Outbound request method not allowed!") + + else + Util.failure(code: :error, + data: Util.error_data_from_response(caller: self.class, response:), + log_message: "Outbound request failed with unknown error!") + end + end + + # rubocop:disable Metrics/AbcSize + def success_result(response, username) + doc = Nokogiri::XML(response.body.to_s) + result = {} + doc.xpath("/d:multistatus/d:response").each do |resource_section| + resource = resource_path(resource_section, username) + result[resource] = {} + + # In future it could be useful to respond not only with found, but not found props as well + # resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 404 Not Found']]/d:prop/*") + resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/*").each do |node| + result[resource][node.name.to_s] = node.text.strip + end + end + + ServiceResult.success(result:) + end + + # rubocop:enable Metrics/AbcSize + + def base_uri = "#{@storage.uri}remote.php/dav/files" + + def resource_path(section, username) + path = CGI.unescape(section.xpath("d:href").text.strip) + .gsub!(Util.join_uri_path(URI(base_uri).path, username), "") + + path.end_with?("/") && path.length > 1 ? path.chop : path + end + + def request_body(props) + Nokogiri::XML::Builder.new do |xml| + xml["d"].propfind( + "xmlns:d" => "DAV:", + "xmlns:oc" => "http://owncloud.org/ns", + "xmlns:nc" => "http://nextcloud.org/ns" + ) do + xml["d"].prop do + props.each do |prop| + namespace, property = prop.split(":") + xml[namespace].send(property) + end + end + end + end.to_xml end end end - end.to_xml - - response = OpenProject - .httpx - .basic_auth(@username, @password) - .with(headers: { "Depth" => depth }) - .request( - "PROPFIND", - UTIL.join_uri_path( - @uri, - "remote.php/dav/files", - CGI.escapeURIComponent(@username), - UTIL.escape_path(path) - ), - xml: body - ) - - error_data = Storages::StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - doc = Nokogiri::XML(response.body.to_s) - result = {} - doc.xpath("/d:multistatus/d:response").each do |resource_section| - resource = CGI.unescape(resource_section.xpath("d:href").text.strip) - .gsub!(UTIL.join_uri_path(@uri.path, "/remote.php/dav/files/#{@username}/"), "") - - result[resource] = {} - - # In future it could be useful to respond not only with found, but not found props as well - # resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 404 Not Found']]/d:prop/*") - resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/*").each do |node| - result[resource][node.name.to_s] = node.text.strip - end - end - - ServiceResult.success(result:) - in { status: 405 } - UTIL.error(:not_allowed, "Outbound request method not allowed", error_data) - in { status: 401 } - UTIL.error(:unauthorized, "Outbound request not authorized", error_data) - in { status: 404 } - UTIL.error(:not_found, "Outbound request destination not found", error_data) - else - UTIL.error(:error, "Outbound request failed", error_data) end end - - # rubocop:enable Metrics/AbcSize end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query_legacy.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query_legacy.rb new file mode 100644 index 00000000000..cb8043a6d90 --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query_legacy.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages::Peripherals::StorageInteraction::Nextcloud::Internal + class PropfindQueryLegacy + UTIL = ::Storages::Peripherals::StorageInteraction::Nextcloud::Util + + # Only for information purposes currently. + # Probably a bit later we could validate `#call` parameters. + # + # DEPTH = %w[0 1 infinity].freeze + # POSSIBLE_PROPS = %w[ + # d:getlastmodified + # d:getetag + # d:getcontenttype + # d:resourcetype + # d:getcontentlength + # d:permissions + # d:size + # oc:id + # oc:fileid + # oc:favorite + # oc:comments-href + # oc:comments-count + # oc:comments-unread + # oc:owner-id + # oc:owner-display-name + # oc:share-types + # oc:checksums + # oc:size + # nc:has-preview + # nc:rich-workspace + # nc:contained-folder-count + # nc:contained-file-count + # nc:acl-list + # ].freeze + + def initialize(storage) + @uri = storage.uri + @username = storage.username + @password = storage.password + @group = storage.group + end + + def self.call(storage:, depth:, path:, props:) + new(storage).call(depth:, path:, props:) + end + + # rubocop:disable Metrics/AbcSize + def call(depth:, path:, props:) + body = Nokogiri::XML::Builder.new do |xml| + xml["d"].propfind( + "xmlns:d" => "DAV:", + "xmlns:oc" => "http://owncloud.org/ns", + "xmlns:nc" => "http://nextcloud.org/ns" + ) do + xml["d"].prop do + props.each do |prop| + namespace, property = prop.split(":") + xml[namespace].send(property) + end + end + end + end.to_xml + + response = OpenProject + .httpx + .basic_auth(@username, @password) + .with(headers: { "Depth" => depth }) + .request( + "PROPFIND", + UTIL.join_uri_path( + @uri, + "remote.php/dav/files", + CGI.escapeURIComponent(@username), + UTIL.escape_path(path) + ), + xml: body + ) + + error_data = Storages::StorageErrorData.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + doc = Nokogiri::XML(response.body.to_s) + result = {} + doc.xpath("/d:multistatus/d:response").each do |resource_section| + resource = CGI.unescape(resource_section.xpath("d:href").text.strip) + .gsub!(UTIL.join_uri_path(@uri.path, "/remote.php/dav/files/#{@username}/"), "") + + result[resource] = {} + + # In future it could be useful to respond not only with found, but not found props as well + # resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 404 Not Found']]/d:prop/*") + resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/*").each do |node| + result[resource][node.name.to_s] = node.text.strip + end + end + + ServiceResult.success(result:) + in { status: 405 } + UTIL.error(:not_allowed, "Outbound request method not allowed", error_data) + in { status: 401 } + UTIL.error(:unauthorized, "Outbound request not authorized", error_data) + in { status: 404 } + UTIL.error(:not_found, "Outbound request destination not found", error_data) + else + UTIL.error(:error, "Outbound request failed", error_data) + end + end + + # rubocop:enable Metrics/AbcSize + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query.rb index efce5928626..ee0ff46320a 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query.rb @@ -28,109 +28,89 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::OneDrive - class FolderFilesFileIdsDeepQuery - FIELDS = %w[id name file folder parentReference].freeze +module Storages + module Peripherals + module StorageInteraction + module OneDrive + class FolderFilesFileIdsDeepQuery + CHILDREN_FIELDS = %w[id name file folder parentReference].freeze + FOLDER_FIELDS = %w[id name parentReference].freeze - def self.call(storage:, folder:) - new(storage).call(folder:) - end + def self.call(storage:, auth_strategy:, folder:) + new(storage).call(auth_strategy:, folder:) + end - def initialize(storage) - @storage = storage - @delegate = Internal::ChildrenQuery.new(storage) - end + def initialize(storage) + @storage = storage + @children_query = Internal::ChildrenQuery.new(storage) + @drive_item_query = Internal::DriveItemQuery.new(storage) + end - def call(folder:) - Util.using_admin_token(@storage) do |http| - fetch_result = fetch_folder(http, folder) - return fetch_result if fetch_result.failure? + def call(auth_strategy:, folder:) + Authentication[auth_strategy].call(storage: @storage) do |http| + fetch_result = fetch_folder(http, folder) + return fetch_result if fetch_result.failure? - file_ids_dictionary = fetch_result.result - queue = [folder] + file_ids_dictionary = fetch_result.result + queue = [folder] - while queue.any? - dir = queue.shift + while queue.any? + dir = queue.shift - visit = visit(http, dir) - return visit if visit.failure? + visit = visit(http, dir) + return visit if visit.failure? - entry, to_queue = visit.result.values_at(:entry, :to_queue) - file_ids_dictionary = file_ids_dictionary.merge(entry) - queue.concat(to_queue) + entry, to_queue = visit.result.values_at(:entry, :to_queue) + file_ids_dictionary = file_ids_dictionary.merge(entry) + queue.concat(to_queue) + end + + ServiceResult.success(result: file_ids_dictionary) + end + end + + private + + def visit(http, folder) + call = @children_query.call(http:, folder:, fields: CHILDREN_FIELDS) + return call if call.failure? + + entry = {} + to_queue = [] + + call.result[:value].each do |json| + new_entry, folder = parse_drive_item_info(json).values_at(:entry, :folder) + + entry = entry.merge(new_entry) + if folder.present? + to_queue.append(folder) + end + end + + ServiceResult.success(result: { entry:, to_queue: }) + end + + def parse_drive_item_info(json) + drive_item_id = json[:id] + location = Util.extract_location(json[:parentReference], json[:name]) + + entry = { location => StorageFileId.new(id: drive_item_id) } + folder = json[:folder].present? ? ParentFolder.new(drive_item_id) : nil + + { entry:, folder: } + end + + def fetch_folder(http, folder) + result = @drive_item_query.call(http:, drive_item_id: folder.path, fields: FOLDER_FIELDS) + result.map do |json| + if folder.root? + { "/" => StorageFileId.new(id: json[:id]) } + else + parse_drive_item_info(json)[:entry] + end + end + end end - - ServiceResult.success(result: file_ids_dictionary) - end - end - - private - - def visit(http, folder) - call = @delegate.call(http:, folder:, fields: FIELDS) - return call if call.failure? - - entry = {} - to_queue = [] - - call.result[:value].each do |json| - new_entry, folder = parse_drive_item_info(json).values_at(:entry, :folder) - - entry = entry.merge(new_entry) - if folder.present? - to_queue.append(folder) - end - end - - ServiceResult.success(result: { entry:, to_queue: }) - end - - def parse_drive_item_info(json) - drive_item_id = json[:id] - location = Util.extract_location(json[:parentReference], json[:name]) - - entry = { location => Storages::StorageFileInfo.from_id(drive_item_id) } - folder = json[:folder].present? ? Storages::Peripherals::ParentFolder.new(drive_item_id) : nil - - { entry:, folder: } - end - - # TODO: REMOVE WITH #51713, as this should be replaced by internal drive item query - # with harmonized interface for authentication - - def fetch_folder(http, folder) - uri_path = if folder.root? - "/v1.0/drives/#{@storage.drive_id}/root" - else - "/v1.0/drives/#{@storage.drive_id}/items/#{folder}" - end - - response = http.get(Util.join_uri_path(@storage.uri, "#{uri_path}?$select=id,name,parentReference")) - handle_responses(response).map do |json| - if folder.root? - { "/" => Storages::StorageFileInfo.from_id(json[:id]) } - else - parse_drive_item_info(json)[:entry] - end - end - end - - def handle_responses(response) - case response - in { status: 200..299 } - ServiceResult.success(result: response.json(symbolize_keys: true)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source: self)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source: self)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source: self)) - else - data = ::Storages::StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: ::Storages::StorageError.new(code: :error, data:)) end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb index 7179d09597b..1c7ae445296 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb @@ -34,8 +34,6 @@ module Storages module OneDrive module Internal class DriveItemQuery - Util = ::Storages::Peripherals::StorageInteraction::OneDrive::Util - def initialize(storage) @storage = storage end @@ -53,11 +51,10 @@ module Storages private def make_file_request(drive_item_id, http, select_url_query) - response = http.get(Util.join_uri_path(@storage.uri, uri_path_for(drive_item_id) + select_url_query)) - handle_responses(response) + handle_response http.get("#{@storage.uri}#{uri_path_for(drive_item_id)}#{select_url_query}") end - def handle_responses(response) + def handle_response(response) case response in { status: 200..299 } ServiceResult.success(result: response.json(symbolize_keys: true)) @@ -71,16 +68,16 @@ module Storages ServiceResult.failure(result: :unauthorized, errors: Util.storage_error(response:, code: :unauthorized, source: self.class)) else - data = ::Storages::StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: ::Storages::StorageError.new(code: :error, data:)) + data = StorageErrorData.new(source: self.class, payload: response) + ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:)) end end def uri_path_for(file_id) if file_id == "/" - "/v1.0/drives/#{@storage.drive_id}/root" + "v1.0/drives/#{@storage.drive_id}/root" else - "/v1.0/drives/#{@storage.drive_id}/items/#{file_id}" + "v1.0/drives/#{@storage.drive_id}/items/#{file_id}" end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb index ae2d0169588..c10c71c4f06 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb @@ -28,86 +28,98 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::OneDrive::Util - using Storages::Peripherals::ServiceResultRefinements +module Storages + module Peripherals + module StorageInteraction + module OneDrive + module Util + using ServiceResultRefinements - class << self - def mime_type(json) - json.dig(:file, :mimeType) || (json.key?(:folder) ? "application/x-op-directory" : nil) - end + class << self + def mime_type(json) + json.dig(:file, :mimeType) || (json.key?(:folder) ? "application/x-op-directory" : nil) + end - def using_user_token(storage, user, &) - connection_manager = ::OAuthClients::ConnectionManager - .new(user:, configuration: storage.oauth_configuration) + def using_user_token(storage, user, &) + connection_manager = ::OAuthClients::ConnectionManager + .new(user:, configuration: storage.oauth_configuration) - connection_manager - .get_access_token - .match( - on_success: ->(token) do - connection_manager.request_with_token_refresh(token) { yield token } - end, - on_failure: ->(_) do - ServiceResult.failure( - result: :unauthorized, - errors: ::Storages::StorageError.new( - code: :unauthorized, - data: ::Storages::StorageErrorData.new(source: connection_manager), - log_message: "Query could not be created! No access token found!" + connection_manager + .get_access_token + .match( + on_success: ->(token) do + connection_manager.request_with_token_refresh(token) { yield token } + end, + on_failure: ->(_) do + ServiceResult.failure( + result: :unauthorized, + errors: StorageError.new( + code: :unauthorized, + data: StorageErrorData.new(source: connection_manager), + log_message: "Query could not be created! No access token found!" + ) + ) + end + ) + end + + def storage_error(response:, code:, source:) + data = StorageErrorData.new(source:, payload: response.json(symbolize_keys: true)) + + StorageError.new(code:, data:) + end + + def join_uri_path(uri, *) + # We use `File.join` to ensure single `/` in between every part. This API will break if executed on a + # Windows context, as it used `\` as file separators. But we anticipate that OpenProject + # Server is not run on a Windows context. + # URI::join cannot be used, as it behaves very different for the path parts depending on trailing slashes. + File.join(uri.to_s, *) + end + + def json_content_type + { headers: { "Content-Type" => "application/json" } } + end + + # rubocop:disable Metrics/AbcSize + def using_admin_token(storage) + oauth_client = storage.oauth_configuration.basic_rack_oauth_client + + token_result = + begin + Rails.cache.fetch("storage.#{storage.id}.access_token", expires_in: 50.minutes) do + ServiceResult.success(result: oauth_client.access_token!(scope: "https://graph.microsoft.com/.default")) + end + rescue Rack::OAuth2::Client::Error => e + ServiceResult.failure(errors: ::Storages::StorageError.new( + code: :unauthorized, + data: StorageErrorData.new(source: self.class), + log_message: e.message + )) + end + + token_result.match( + on_success: ->(token) do + yield OpenProject.httpx.with(origin: storage.uri, + headers: { authorization: "Bearer #{token.access_token}", + accept: "application/json", + "content-type": "application/json" }) + end, + on_failure: ->(errors) { ServiceResult.failure(result: :unauthorized, errors:) } ) - ) + end + + # rubocop:enable Metrics/AbcSize + + def extract_location(parent_reference, file_name = "") + location = parent_reference[:path].gsub(/.*root:/, "") + + appendix = file_name.blank? ? "" : "/#{file_name}" + location.empty? ? "/#{file_name}" : "#{location}#{appendix}" + end end - ) - end - - def storage_error(response:, code:, source:) - data = ::Storages::StorageErrorData.new(source:, payload: response.json(symbolize_keys: true)) - - ::Storages::StorageError.new(code:, data:) - end - - def join_uri_path(uri, *) - # We use `File.join` to ensure single `/` in between every part. This API will break if executed on a - # Windows context, as it used `\` as file separators. But we anticipate that OpenProject - # Server is not run on a Windows context. - # URI::join cannot be used, as it behaves very different for the path parts depending on trailing slashes. - File.join(uri.to_s, *) - end - - def json_content_type - { headers: { "Content-Type" => "application/json" } } - end - - def using_admin_token(storage) - oauth_client = storage.oauth_configuration.basic_rack_oauth_client - - token_result = begin - Rails.cache.fetch("storage.#{storage.id}.access_token", expires_in: 50.minutes) do - ServiceResult.success(result: oauth_client.access_token!(scope: "https://graph.microsoft.com/.default")) end - rescue Rack::OAuth2::Client::Error => e - ServiceResult.failure(errors: ::Storages::StorageError.new( - code: :unauthorized, - data: ::Storages::StorageErrorData.new(source: self.class), - log_message: e.message - )) end - - token_result.match( - on_success: ->(token) do - yield OpenProject.httpx.with(origin: storage.uri, - headers: { authorization: "Bearer #{token.access_token}", - accept: "application/json", - "content-type": "application/json" }) - end, - on_failure: ->(errors) { ServiceResult.failure(result: :unauthorized, errors:) } - ) - end - - def extract_location(parent_reference, file_name = "") - location = parent_reference[:path].gsub(/.*root:/, "") - - appendix = file_name.blank? ? "" : "/#{file_name}" - location.empty? ? "/#{file_name}" : "#{location}#{appendix}" end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb b/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb index 5a38f9e8ac6..c139fc16ef0 100644 --- a/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb +++ b/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb @@ -26,26 +26,28 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals - class ParentFolder - attr_reader :path +module Storages + module Peripherals + class ParentFolder + attr_reader :path - def initialize(path) - @path = path + def initialize(path) + @path = path + end + + def root? + @path == "/" + end + + def to_s + @path + end end - def root? - @path == "/" - end - - def to_s - @path - end - end - - module StorageParentFolderExtractor - def extract_parent_folder(params) - ParentFolder.new(params[:parent].presence || "/") + module StorageParentFolderExtractor + def extract_parent_folder(params) + ParentFolder.new(params[:parent].presence || "/") + end end end end diff --git a/modules/storages/app/models/storages/storage_file_id.rb b/modules/storages/app/models/storages/storage_file_id.rb new file mode 100644 index 00000000000..43efc022148 --- /dev/null +++ b/modules/storages/app/models/storages/storage_file_id.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + StorageFileId = Data.define(:id) +end diff --git a/modules/storages/app/services/storages/file_links/copy_file_links_service.rb b/modules/storages/app/services/storages/file_links/copy_file_links_service.rb index 4be3db694ce..7ff89713680 100644 --- a/modules/storages/app/services/storages/file_links/copy_file_links_service.rb +++ b/modules/storages/app/services/storages/file_links/copy_file_links_service.rb @@ -46,10 +46,10 @@ module Storages def call source_file_links = FileLink - .includes(:creator) - .where(storage: @source.storage, - container_id: @work_packages_map.keys, - container_type: "WorkPackage") + .includes(:creator) + .where(storage: @source.storage, + container_id: @work_packages_map.keys, + container_type: "WorkPackage") with_locale_for(@user) do if @source.project_folder_automatic? @@ -62,6 +62,7 @@ module Storages private + # rubocop:disable Metrics/AbcSize def create_managed_file_links(source_file_links) location_map = build_location_map( source_files_info(source_file_links).result, @@ -83,6 +84,9 @@ module Storages end end + # rubocop:enable Metrics/AbcSize + + # rubocop:disable Metrics/AbcSize def build_location_map(source_files, target_location_map) # We need this due to inconsistencies of how we represent the File Path target_location_map.transform_keys! { |key| key.starts_with?("/") ? key : "/#{key}" } @@ -99,6 +103,8 @@ module Storages end end + # rubocop:enable Metrics/AbcSize + def auth_strategy Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken .strategy @@ -114,7 +120,7 @@ module Storages def target_files_map Peripherals::Registry - .resolve("#{@source.storage.short_provider_type}.queries.folder_files_file_ids_deep_query") + .resolve("#{@source.storage.short_provider_type}.queries.folder_files_file_ids_deep") .call(storage: @source.storage, folder: Peripherals::ParentFolder.new(@target.project_folder_location)) end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb index 615493b1a1a..f656881dc24 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand, :vcr, :webmock do +RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand, :webmock do let(:user) { create(:user) } let(:storage) do create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) @@ -40,14 +40,14 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) end - it_behaves_like "basic command setup" + it_behaves_like "create_folder_command: basic command setup" context "when creating a folder in the root", vcr: "nextcloud/create_folder_root" do let(:folder_name) { "Földer CreatedBy Çommand" } let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } let(:path) { "/#{folder_name}" } - it_behaves_like "successful folder creation" + it_behaves_like "create_folder_command: successful folder creation" end context "when creating a folder in a parent folder", vcr: "nextcloud/create_folder_parent" do @@ -55,7 +55,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde let(:parent_location) { Storages::Peripherals::ParentFolder.new("/Folder") } let(:path) { "/Folder/#{folder_name}" } - it_behaves_like "successful folder creation" + it_behaves_like "create_folder_command: successful folder creation" end context "when creating a folder in a non-existing parent folder", vcr: "nextcloud/create_folder_parent_not_found" do @@ -63,7 +63,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde let(:parent_location) { Storages::Peripherals::ParentFolder.new("/DeathStar3") } let(:error_source) { described_class } - it_behaves_like "parent not found" + it_behaves_like "create_folder_command: parent not found" end context "when folder already exists", vcr: "nextcloud/create_folder_already_exists" do @@ -71,7 +71,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } let(:error_source) { described_class } - it_behaves_like "folder already exists" + it_behaves_like "create_folder_command: folder already exists" end private diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query_spec.rb new file mode 100644 index 00000000000..419236e1967 --- /dev/null +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FolderFilesFileIdsDeepQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:auth_strategy) do + Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) + end + + it_behaves_like "folder_files_file_ids_deep_query: basic query setup" + + context "with parent folder being root", vcr: "nextcloud/folder_files_file_ids_deep_query_root" do + let(:folder) { Storages::Peripherals::ParentFolder.new("/") } + let(:expected_ids) do + { + "/" => "2", + "/Folder with spaces" => "165", + "/Folder with spaces/New Requests" => "166", + "/Folder with spaces/New Requests/request_001.md" => "167", + "/Folder with spaces/New Requests/request_002.md" => "168", + "/Folder" => "169", + "/Folder/android-studio-2021.3.1.17-linux.tar.gz" => "267", + "/Folder/empty" => "172", + "/Folder/Ümlæûts" => "350", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351", + "/Practical_guide_to_BAGGM_Digital.pdf" => "295", + "/Readme.md" => "268" + } + end + + it_behaves_like "folder_files_file_ids_deep_query: successful query" + end + + context "with a given parent folder", vcr: "nextcloud/folder_files_file_ids_deep_query_parent_folder" do + let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder") } + let(:expected_ids) do + { + "/Folder" => "169", + "/Folder/android-studio-2021.3.1.17-linux.tar.gz" => "267", + "/Folder/empty" => "172", + "/Folder/Ümlæûts" => "350", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351" + } + end + + it_behaves_like "folder_files_file_ids_deep_query: successful query" + end + + context "with not existent parent folder", vcr: "nextcloud/folder_files_file_ids_deep_query_invalid_parent" do + let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } + let(:error_source) { Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery } + + it_behaves_like "folder_files_file_ids_deep_query: not found" + end +end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb index 6e3fcb92f09..d96b8e27b2a 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb @@ -31,20 +31,20 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolderCommand, :vcr, :webmock do +RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolderCommand, :webmock do let(:storage) { create(:sharepoint_dev_drive_storage) } let(:auth_strategy) do Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy end - it_behaves_like "basic command setup" + it_behaves_like "create_folder_command: basic command setup" context "when creating a folder in the root", vcr: "one_drive/create_folder_root" do let(:folder_name) { "Földer CreatedBy Çommand" } let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } let(:path) { "/#{folder_name}" } - it_behaves_like "successful folder creation" + it_behaves_like "create_folder_command: successful folder creation" end context "when creating a folder in a parent folder", vcr: "one_drive/create_folder_parent" do @@ -52,7 +52,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolder let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU") } let(:path) { "/Folder with spaces/#{folder_name}" } - it_behaves_like "successful folder creation" + it_behaves_like "create_folder_command: successful folder creation" end context "when creating a folder in a non-existing parent folder", vcr: "one_drive/create_folder_parent_not_found" do @@ -60,7 +60,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolder let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF4A7ZCWVBXRTEU") } let(:error_source) { described_class } - it_behaves_like "parent not found" + it_behaves_like "create_folder_command: parent not found" end context "when folder already exists", vcr: "one_drive/create_folder_already_exists" do @@ -68,7 +68,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolder let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } let(:error_source) { described_class } - it_behaves_like "folder already exists" + it_behaves_like "create_folder_command: folder already exists" end private diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query_spec.rb index a19b6228469..09ce762e65e 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/folder_files_file_ids_deep_query_spec.rb @@ -32,126 +32,66 @@ require "spec_helper" require_module_spec_helper RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::FolderFilesFileIdsDeepQuery, :webmock do - using Storages::Peripherals::ServiceResultRefinements + let(:storage) { create(:sharepoint_dev_drive_storage) } + let(:auth_strategy) do + Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy + end - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:folder) { Storages::Peripherals::ParentFolder.new("/") } + it_behaves_like "folder_files_file_ids_deep_query: basic query setup" - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq folder]) + context "with parent folder being root", vcr: "one_drive/folder_files_file_ids_deep_query_root" do + let(:folder) { Storages::Peripherals::ParentFolder.new("/") } + let(:expected_ids) do + { + "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", + "/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", + "/Folder with spaces/very empty folder" => "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H", + "/Folder with spaces/wordle1.png" => "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN", + "/Folder with spaces/wordle2.png" => "01AZJL5PIIFUD6A765KBAIAEMYACAFB2WP", + "/Folder with spaces/wordle3.png" => "01AZJL5PL4AUJEU43CQZFJKN7BQPRP3BLF", + "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", + "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", + "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", + "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", + "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", + "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", + "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", + "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", + "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", + "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE", + "/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB" + } end - context "with outbound requests successful" do - subject do - described_class.call(storage:, folder:).result - end + it_behaves_like "folder_files_file_ids_deep_query: successful query" + end - context "with parent folder being root", vcr: "one_drive/folder_files_file_ids_deep_query_root" do - it "returns the file id dictionary" do - expect(subject.transform_values(&:id)) - .to eq({ - "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", - "/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", - "/Folder with spaces/very empty folder" => "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H", - "/Folder with spaces/wordle1.png" => "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN", - "/Folder with spaces/wordle2.png" => "01AZJL5PIIFUD6A765KBAIAEMYACAFB2WP", - "/Folder with spaces/wordle3.png" => "01AZJL5PL4AUJEU43CQZFJKN7BQPRP3BLF", - "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", - "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", - "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", - "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", - "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", - "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", - "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", - "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", - "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", - "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", - "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE", - "/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB" - }) - end - end - - context "with a given parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_parent_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR") } - - it "returns the file id dictionary" do - expect(subject.transform_values(&:id)) - .to eq({ - "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", - "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", - "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", - "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", - "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", - "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", - "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", - "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", - "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", - "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", - "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE" - }) - end - end + context "with a given parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_parent_folder" do + let(:folder) { Storages::Peripherals::ParentFolder.new("01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR") } + let(:expected_ids) do + { + "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", + "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", + "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", + "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", + "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", + "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", + "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", + "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", + "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", + "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE" + } end - context "with not existent parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_invalid_parent" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } + it_behaves_like "folder_files_file_ids_deep_query: successful query" + end - it "must return not found" do - result = described_class.call(storage:, folder:) - expect(result).to be_failure - expect(result.error_source).to be_a(described_class) + context "with not existent parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_invalid_parent" do + let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } + let(:error_source) { Storages::Peripherals::StorageInteraction::OneDrive::Internal::DriveItemQuery } - result.match( - on_failure: ->(error) { expect(error.code).to eq(:not_found) }, - on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" } - ) - end - end - - context "with invalid oauth credentials", vcr: "one_drive/folder_files_file_ids_deep_query_invalid_credentials" do - before do - unauthorized_http = OpenProject.httpx.with(origin: storage.uri, - headers: { authorization: "Bearer YouShallNotPass", - accept: "application/json", - "content-type": "application/json" }) - allow(Storages::Peripherals::StorageInteraction::OneDrive::Util) - .to receive(:using_admin_token) - .and_yield(unauthorized_http) - end - - it "must return unauthorized" do - result = described_class.call(storage:, folder:) - expect(result).to be_failure - expect(result.error_source).to be_a(described_class) - - result.match( - on_failure: ->(error) { expect(error.code).to eq(:unauthorized) }, - on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" } - ) - end - end - - context "with network errors" do - before do - request = HTTPX::Request.new(:get, "https://my.timeout.org/") - httpx_double = class_double(HTTPX, get: HTTPX::ErrorResponse.new(request, "Timeout happens", {})) - allow(Storages::Peripherals::StorageInteraction::OneDrive::Util) - .to receive(:using_admin_token) - .and_yield(httpx_double) - end - - it "must return an error with wrapped network error response" do - error = described_class.call(storage:, folder:) - expect(error).to be_failure - expect(error.result).to eq(:error) - expect(error.error_payload).to be_a(HTTPX::ErrorResponse) - end - end + it_behaves_like "folder_files_file_ids_deep_query: not found" end end diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_invalid_parent.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_invalid_parent.yml new file mode 100644 index 00000000000..f10e1ae12f9 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_invalid_parent.yml @@ -0,0 +1,95 @@ +--- +http_interactions: +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/admin/I/just/made/that/up + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Depth: + - infinity + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 404 + message: Not Found + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Mon, 29 Apr 2024 09:27:02 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=eLXRRbs1vLi5xkH0WJYyVwQJqpjGro0c0Zg3nUyYr5VYgUqF9Bt2M8oZ9gNzG3r%2BetStTAiHiIzJoK9h3r4uj41hg%2Bp0rmSAbfcYTXntvL8N3gQIW%2BkkeMHfhya3npEJ; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '231' + body: + encoding: UTF-8 + string: | + + + Sabre\DAV\Exception\NotFound + File with name //I could not be located + + recorded_at: Mon, 29 Apr 2024 09:27:02 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_parent_folder.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_parent_folder.yml new file mode 100644 index 00000000000..a1fd79fd7d9 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_parent_folder.yml @@ -0,0 +1,98 @@ +--- +http_interactions: +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/admin/Folder + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Depth: + - infinity + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 207 + message: Multi-Status + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Mon, 29 Apr 2024 09:27:02 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=QGRWLsaUpaf5JsdI0cV162JoZvcVfhQVV61E6mpPh%2BvmQRlBwBWC%2BP42vMFWwOmkkWTTF9miI%2FBEoiqthA3UQ3ib0UiO40kKObozzqQBv5d%2FsnkiMF0pJJMnsdM8JWTT; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - C2A5yYZ2GjWsVq5rexEF + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - C2A5yYZ2GjWsVq5rexEF + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '366' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/admin/Folder/169HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/android-studio-2021.3.1.17-linux.tar.gz267HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/empty/172HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/350HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx351HTTP/1.1 200 OKHTTP/1.1 404 Not Found + recorded_at: Mon, 29 Apr 2024 09:27:02 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_root.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_root.yml new file mode 100644 index 00000000000..ba95d5d2c2a --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/folder_files_file_ids_deep_query_root.yml @@ -0,0 +1,98 @@ +--- +http_interactions: +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/admin/ + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Depth: + - infinity + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 207 + message: Multi-Status + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Mon, 29 Apr 2024 09:21:37 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=qHbGrRqzApLz65yOgbjo0F50qZOP7tl5seLjGa5MF5xAsfWUBS86U4TbnP6wBuwrqH1oAgAR19gOq6FYJvymPZW3tkMSKR9kS5YaIuyIWuqjif2txirDkqaymU7cOOVG; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 6Cb1TBf2Ullw34vWD3yf + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - 6Cb1TBf2Ullw34vWD3yf + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '477' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/admin/2HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/169HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/android-studio-2021.3.1.17-linux.tar.gz267HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/empty/172HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/350HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx351HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder%20with%20spaces/165HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/166HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/request_001.md167HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/request_002.md168HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Practical_guide_to_BAGGM_Digital.pdf295HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Readme.md268HTTP/1.1 200 OKHTTP/1.1 404 Not Found + recorded_at: Mon, 29 Apr 2024 09:21:38 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb b/modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb index 1ef93d5e417..823dfc1eebf 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "basic command setup" do +RSpec.shared_examples_for "create_folder_command: basic command setup" do it "is registered as commands.create_folder" do expect(Storages::Peripherals::Registry .resolve("#{storage.short_provider_type}.commands.create_folder")).to eq(described_class) @@ -45,7 +45,7 @@ RSpec.shared_examples_for "basic command setup" do end end -RSpec.shared_examples_for "successful folder creation" do +RSpec.shared_examples_for "create_folder_command: successful folder creation" do it "creates a folder" do result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:) @@ -60,7 +60,7 @@ RSpec.shared_examples_for "successful folder creation" do end end -RSpec.shared_examples_for "parent not found" do +RSpec.shared_examples_for "create_folder_command: parent not found" do it "returns a failure" do result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:) @@ -72,7 +72,7 @@ RSpec.shared_examples_for "parent not found" do end end -RSpec.shared_examples_for "folder already exists" do +RSpec.shared_examples_for "create_folder_command: folder already exists" do it "returns a failure" do result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:) diff --git a/modules/storages/spec/support/shared_examples_for_adapters/folder_files_file_ids_deep_query_examples.rb b/modules/storages/spec/support/shared_examples_for_adapters/folder_files_file_ids_deep_query_examples.rb new file mode 100644 index 00000000000..3f710617531 --- /dev/null +++ b/modules/storages/spec/support/shared_examples_for_adapters/folder_files_file_ids_deep_query_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +RSpec.shared_examples_for "folder_files_file_ids_deep_query: basic query setup" do + it "is registered as queries.folder_files_file_ids_deep" do + expect(Storages::Peripherals::Registry + .resolve("#{storage.short_provider_type}.queries.folder_files_file_ids_deep")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq folder]) + end +end + +RSpec.shared_examples_for "folder_files_file_ids_deep_query: successful query" do + it "returns a map of locations to file ids" do + result = described_class.call(storage:, auth_strategy:, folder:) + + expect(result).to be_success + + response = result.result + expect(response.transform_values(&:id)).to eq(expected_ids) + end +end + +RSpec.shared_examples_for "folder_files_file_ids_deep_query: not found" do + it "returns a failure" do + result = described_class.call(storage:, auth_strategy:, folder:) + + expect(result).to be_failure + + error = result.errors + expect(error.code).to eq(:not_found) + expect(error.data.source).to eq(error_source) + end +end diff --git a/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb b/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb index 5132c6491ba..64be7ac0b05 100644 --- a/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb +++ b/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb @@ -123,7 +123,7 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job_ba describe "managed project folders" do before do Storages::Peripherals::Registry - .stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep_query", ->(storage:, folder:) { + .stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep", ->(storage:, folder:) { ServiceResult.success(result: target_deep_file_ids) }) @@ -177,7 +177,7 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job_ba }) Storages::Peripherals::Registry - .stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep_query", ->(storage:, folder:) { + .stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep", ->(storage:, folder:) { ServiceResult.success(result: target_deep_file_ids) }) From 3e676a2070e67880bfc8eccf656cfe8173340313 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Mon, 29 Apr 2024 13:01:24 +0200 Subject: [PATCH 2/3] [#54355] fixed test expectation --- .../nextcloud_group_folder_properties_sync_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb b/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb index d9608269676..9f1f0178c65 100644 --- a/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb @@ -674,7 +674,7 @@ RSpec.describe Storages::NextcloudGroupFolderPropertiesSyncService, :webmock do expect(OpenProject.logger) .to have_received(:warn) .with(folder: "OpenProject", - command: Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery, + command: Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQueryLegacy, message: "Outbound request destination not found", data: { status: 404, body: "" }) end From c9bb11ecf728c0a9124ce9e80e7851b8f6e389a9 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Tue, 30 Apr 2024 13:50:42 +0200 Subject: [PATCH 3/3] [#54355] improved code readability --- docker/dev/tls/docker-compose.yml | 2 -- .../nextcloud/folder_files_file_ids_deep_query.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docker/dev/tls/docker-compose.yml b/docker/dev/tls/docker-compose.yml index a4ab21cebd3..d9172f23f87 100644 --- a/docker/dev/tls/docker-compose.yml +++ b/docker/dev/tls/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: traefik: image: traefik:latest diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb index 4116284b833..4fa4a0dc2bd 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb @@ -44,8 +44,8 @@ module Storages def call(auth_strategy:, folder:) origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - if origin_user_id.failure? - return origin_user_id + .on_failure do |result| + return result end Authentication[auth_strategy].call(storage: @storage, http_options:) do |http|