Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[chore] Refactor nextcloud files query spec to vcr #14265

Merged
merged 2 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def self.call(storage:, user:, folder:)
new(storage).call(user:, folder:)
end

# rubocop:disable Metrics/AbcSize
def call(user:, folder:)
result = Util.token(user:, configuration: @configuration) do |token|
base_path = Util.join_uri_path(@uri.path, "remote.php/dav/files")
Expand All @@ -52,25 +51,29 @@ def call(user:, folder:)
}
)

case response
when Net::HTTPSuccess
ServiceResult.success(result: response.body)
when Net::HTTPNotFound
Util.error(:not_found)
when Net::HTTPUnauthorized
Util.error(:unauthorized)
else
Util.error(:error)
end
handle_response(response)
end

storage_files(result)
end

# rubocop:enable Metrics/AbcSize

private

def handle_response(response)
error_data = Storages::StorageErrorData.new(source: self, payload: response)

case response
when Net::HTTPSuccess
ServiceResult.success(result: response.body)
when Net::HTTPNotFound
Util.error(:not_found, 'Outbound request destination not found', error_data)
when Net::HTTPUnauthorized
Util.error(:unauthorized, 'Outbound request not authorized', error_data)
else
Util.error(:error, 'Outbound request failed', error_data)
end
end

def requested_folder(folder)
return '' if folder.root?

Expand Down Expand Up @@ -132,7 +135,7 @@ def forge_ancestor(location)
end

def name(location)
location == '/' ? location : CGI.unescape(location.split('/').last)
location == '/' ? 'Root' : CGI.unescape(location.split('/').last)
end

def storage_file(file_element)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ def token(user:, configuration:, &)
on_success: ->(token) do
connection_manager.request_with_token_refresh(token) { yield token }
end,
on_failure: ->(_) { error(:unauthorized, 'Query could not be created! No access token found!') }
on_failure: ->(_) do
error(:unauthorized,
'Query could not be created! No access token found!',
Storages::StorageErrorData.new(source: connection_manager))
end
)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,131 +31,208 @@
require 'spec_helper'
require_module_spec_helper

RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FilesQuery, :webmock do
let(:storage) { create(:nextcloud_storage, :with_oauth_client) }
let(:folder) { Storages::Peripherals::ParentFolder.new('/') }
let(:user) { create(:user) }
let(:token) do
create(:oauth_client_token, user:, oauth_client: storage.oauth_client, origin_user_id: 'darth@vader with spaces')
end

let(:origin_user_id) { 'darth@vader with spaces' }
let(:webdav_success_response) { create(:webdav_data, parent_path: '', root_path: '', origin_user_id:) }

subject(:files_query) { described_class }

before do
uri = "#{storage.host}/remote.php/dav/files/darth@vader%20with%20spaces/"
allow(Storages::Peripherals::StorageInteraction::Nextcloud::Util).to receive(:token).and_yield(token)
stub_request(:propfind, uri).to_return(status: 207, body: webdav_success_response, headers: {})
end

it '.call requires 3 arguments: storage, user, and folder' do
expect(described_class).to respond_to(:call)

method = described_class.method(:call)
expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq user], %i[keyreq folder])
end

it 'returns a list of files and folders' do
storage_files = files_query.call(storage:, folder:, user:).result
expect(storage_files).to be_a(Storages::StorageFiles)
RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FilesQuery, :vcr, :webmock do
using Storages::Peripherals::ServiceResultRefinements

expect(storage_files.files.size).to eq(4)
expect(storage_files.ancestors.size).to eq(0)
expect(storage_files.parent.location).to eq('/')

mime_types = storage_files.files.map(&:mime_type).uniq!

expect(mime_types).to include('application/pdf') # file
expect(mime_types).to include('application/x-op-directory') # folder
end

it 'returns permissions for each' do
storage_files = files_query.call(storage:, folder:, user:).result

writeable_folder = storage_files.files.find { |file| file.mime_type == 'application/x-op-directory' }
expect(writeable_folder.permissions).to match_array(%i[readable writeable])

readonly_file = storage_files.files.find { |file| file.mime_type == 'application/pdf' }
expect(readonly_file.permissions).to match_array(%i[readable])
let(:user) { create(:user) }
let(:storage) do
create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user)
end
let(:folder) { Storages::Peripherals::ParentFolder.new('/') }

context 'when requesting a sub-folder' do
let(:folder) { Storages::Peripherals::ParentFolder.new('/Photos/Birds') }
let(:webdav_subfolder_success_response) { create(:webdav_data, parent_path: folder.path, root_path: '', origin_user_id:) }
describe '#call' do
it 'responds with correct parameters' do
expect(described_class).to respond_to(:call)

before do
uri = "#{storage.host}/remote.php/dav/files/darth@vader%20with%20spaces#{folder.path}"
stub_request(:propfind, uri).to_return(status: 207, body: webdav_subfolder_success_response, headers: {})
method = described_class.method(:call)
expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq user], %i[keyreq folder])
end

subject(:query_result) { files_query.call(user:, storage:, folder:).result }

it 'returns 2 ancestors' do
ancestors = query_result.ancestors
context 'with outbound requests successful' do
context 'with parent folder being root', vcr: 'nextcloud/files_query_root' do
# rubocop:disable RSpec/ExampleLength
it 'returns a StorageFiles object for root' do
storage_files = described_class.call(storage:, user:, folder:).result

expect(storage_files).to be_a(Storages::StorageFiles)
expect(storage_files.ancestors).to be_empty
expect(storage_files.parent.name).to eq("Root")

expect(storage_files.files.size).to eq(4)
expect(storage_files.files.map(&:to_h))
.to eq([
{
id: '172',
name: 'Folder',
size: 982713473,
created_at: nil,
created_by_name: 'admin',
last_modified_at: '2023-11-29T15:31:30Z',
last_modified_by_name: nil,
location: '/Folder',
mime_type: 'application/x-op-directory',
permissions: %i[readable writeable]
}, {
id: '173',
name: 'Folder with spaces',
size: 74,
created_at: nil,
created_by_name: 'admin',
last_modified_at: '2023-11-29T15:42:21Z',
last_modified_by_name: nil,
location: '/Folder%20with%20spaces',
mime_type: 'application/x-op-directory',
permissions: %i[readable writeable]
}, {
id: '211',
name: 'Practical_guide_to_BAGGM_Digital.pdf',
size: 154592937,
created_at: nil,
created_by_name: 'admin',
last_modified_at: '2022-08-09T06:53:12Z',
last_modified_by_name: nil,
location: '/Practical_guide_to_BAGGM_Digital.pdf',
mime_type: 'application/pdf',
permissions: %i[readable writeable]
}, {
id: '178',
name: 'Readme.md',
size: 31,
created_at: nil,
created_by_name: 'admin',
last_modified_at: '2023-11-29T15:29:16Z',
last_modified_by_name: nil,
location: '/Readme.md',
mime_type: 'text/markdown',
permissions: %i[readable writeable]
}
])
end
# rubocop:enable RSpec/ExampleLength
end

expect(ancestors.size).to eq(2)
expect(ancestors.map(&:location)).to match_array(%w[/ /Photos])
expect(ancestors.map(&:name)).to match_array(%w[/ Photos])
end
context 'with a given parent folder', vcr: 'nextcloud/files_query_parent_folder' do
let(:folder) { Storages::Peripherals::ParentFolder.new('/Folder with spaces/New Requests') }

subject do
described_class.call(storage:, user:, folder:).result
end

# rubocop:disable RSpec/ExampleLength
it 'returns the files content' do
expect(subject.files.size).to eq(2)
expect(subject.files.map(&:to_h))
.to eq([
{
id: '181',
name: 'request_001.md',
size: 48,
created_at: nil,
created_by_name: 'admin',
last_modified_at: '2023-11-29T15:35:25Z',
last_modified_by_name: nil,
location: '/Folder%20with%20spaces/New%20Requests/request_001.md',
mime_type: 'text/markdown',
permissions: %i[readable writeable]
}, {
id: '182',
name: 'request_002.md',
size: 26,
created_at: nil,
created_by_name: 'admin',
last_modified_at: '2023-11-29T15:35:34Z',
last_modified_by_name: nil,
location: '/Folder%20with%20spaces/New%20Requests/request_002.md',
mime_type: 'text/markdown',
permissions: %i[readable writeable]
}
])
end
# rubocop:enable RSpec/ExampleLength

it 'returns ancestors with a forged id' do
expect(subject.ancestors.map { |a| { id: a.id, name: a.name, location: a.location } })
.to eq([
{
id: '8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1',
name: 'Root',
location: '/'
}, {
id: 'c8776f1f6dd36c023c6615d39f01a71d68dd1707b232115b7a4f58bc6da94e2e',
name: 'Folder with spaces',
location: '/Folder%20with%20spaces'
}
])
end

it 'returns the parent itself' do
expect(subject.parent.id).to eq('180')
expect(subject.parent.name).to eq('New Requests')
expect(subject.parent.location).to eq('/Folder%20with%20spaces/New%20Requests')
end
end

it 'returns the parent folder' do
expect(query_result.parent.name).to eq('Birds')
expect(query_result.parent.location).to eq('/Photos/Birds')
end
context 'with parent folder being empty', vcr: 'nextcloud/files_query_empty_folder' do
let(:folder) { Storages::Peripherals::ParentFolder.new('/Folder/empty') }

it 'lists the contents of the folder' do
expect(query_result.files).to all(be_a(Storages::StorageFile))
expect(query_result.files.size).to eq(4)
end
it 'returns an empty StorageFiles object with parent and ancestors' do
storage_files = described_class.call(storage:, user:, folder:).result

it 'the files "location" include the entire path and the file name' do
expect(query_result.files.last.location).to eq("/Photos/Birds/Manual.pdf")
expect(storage_files).to be_a(Storages::StorageFiles)
expect(storage_files.files).to be_empty
expect(storage_files.parent.id).to eq('174')
expect(storage_files.ancestors.map(&:name)).to eq(%w[Root Folder])
end
end
end
end

context 'when the storage runs on a subfolder' do
let(:storage) { create(:nextcloud_storage, :with_oauth_client, host: 'https://example.com/death_star_blueprints') }
context 'with not existent parent folder', vcr: 'nextcloud/files_query_invalid_parent' do
let(:folder) { Storages::Peripherals::ParentFolder.new('/I/just/made/that/up') }

it 'just works' do
storage_files = files_query.call(storage:, user:, folder:)
it 'must return not found' do
result = described_class.call(storage:, user:, folder:)
expect(result).to be_failure
expect(result.error_source).to be_a(described_class)

expect(storage_files).to be_success
result.match(
on_failure: ->(error) { expect(error.code).to eq(:not_found) },
on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" }
)
end
end
end

describe 'with missing OAuth token' do
before do
allow(Storages::Peripherals::StorageInteraction::Nextcloud::Util)
.to receive(:token)
.and_return(ServiceResult.failure(result: :unauthorized,
errors: Storages::StorageError.new(code: :unauthorized)))
end
context 'with invalid oauth token', vcr: 'nextcloud/files_query_invalid_token' do
before do
token = build_stubbed(:oauth_client_token, oauth_client: storage.oauth_client)
allow(Storages::Peripherals::StorageInteraction::Nextcloud::Util)
.to receive(:token).and_yield(token)
end

it 'returns an ":unauthorized" ServiceResult' do
result = files_query.call(folder:, user:, storage:)
expect(result).to be_failure
expect(result.errors.code).to be(:unauthorized)
end
end
it 'must return unauthorized' do
result = described_class.call(storage:, user:, folder:)
expect(result).to be_failure
expect(result.error_source).to be_a(described_class)

shared_examples_for 'outbound is failing' do |code = 500, symbol = :error|
describe "with outbound request returning #{code}" do
before do
uri = "#{storage.host}/remote.php/dav/files/darth@vader%20with%20spaces/"
stub_request(:propfind, uri).to_return(status: code)
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 not existent oauth token' do
let(:user_without_token) { create(:user) }

it "must return :#{symbol} ServiceResult" do
result = files_query.call(folder:, user:, storage:)
it 'must return unauthorized' do
result = described_class.call(storage:, user: user_without_token, folder:)
expect(result).to be_failure
expect(result.errors.code).to be(symbol)
expect(result.error_source).to be_a(OAuthClients::ConnectionManager)

result.match(
on_failure: ->(error) { expect(error.code).to eq(:unauthorized) },
on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" }
)
end
end
end

include_examples 'outbound is failing', 404, :not_found
include_examples 'outbound is failing', 401, :unauthorized
include_examples 'outbound is failing', 500, :error
end
Loading
Loading