Skip to content

Commit dc9eda5

Browse files
authored
Implemetns the AMPF group checks (opf#18435)
* Introduces the AMPF series of tests * Adapts to the new API * Incorporates feedback
1 parent 16c42de commit dc9eda5

File tree

4 files changed

+255
-2
lines changed

4 files changed

+255
-2
lines changed

modules/storages/app/common/storages/peripherals/connection_validators/check_result.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ def self.skipped(key)
3838
end
3939

4040
def self.failure(key, message)
41-
new(key:, state: :failure, message: message, timestamp: Time.zone.now)
41+
new(key:, state: :failure, message:, timestamp: Time.zone.now)
4242
end
4343

4444
def self.success(key)
4545
new(key:, state: :success, message: nil, timestamp: Time.zone.now)
4646
end
4747

4848
def self.warning(key, message)
49-
new(key:, state: :warning, message: message, timestamp: Time.zone.now)
49+
new(key:, state: :warning, message:, timestamp: Time.zone.now)
5050
end
5151

5252
def success? = state == :success
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
module Storages
32+
module Peripherals
33+
module ConnectionValidators
34+
module Nextcloud
35+
class AmpfConnectionValidator < BaseValidator
36+
using ServiceResultRefinements
37+
38+
private
39+
40+
def validate
41+
register_checks(
42+
:userless_access, :group_folder_presence, :files_request, :group_folder_contents
43+
)
44+
45+
userless_access_denied
46+
group_folder_not_found
47+
files_request_failed_with_unknown_error
48+
with_unexpected_content
49+
end
50+
51+
def userless_access_denied
52+
if files.result == :unauthorized
53+
fail_check(:userless_access, message(:userless_access_denied))
54+
else
55+
pass_check(:userless_access)
56+
end
57+
end
58+
59+
def group_folder_not_found
60+
if files.result == :not_found
61+
fail_check(:group_folder_presence, message(:group_folder_not_found))
62+
else
63+
pass_check(:group_folder_presence)
64+
end
65+
end
66+
67+
def files_request_failed_with_unknown_error
68+
if files.result == :error
69+
error "Connection validation failed with unknown error:\n" \
70+
"\tstorage: ##{@storage.id} #{@storage.name}\n" \
71+
"\trequest: Group folder content\n" \
72+
"\tstatus: #{files.result}\n" \
73+
"\tresponse: #{files.error_payload}"
74+
75+
fail_check(:files_request, message(:unknown_error))
76+
else
77+
pass_check(:files_request)
78+
end
79+
end
80+
81+
def with_unexpected_content
82+
unexpected_files = files.result.files.reject { managed_project_folder_ids.include?(it.id) }
83+
return pass_check(:group_folder_contents) if unexpected_files.empty?
84+
85+
log_extraneous_files(unexpected_files)
86+
warn_check(:group_folder_contents, message(:unexpected_content))
87+
end
88+
89+
def log_extraneous_files(unexpected_files)
90+
file_representation = unexpected_files.map do |file|
91+
"Name: #{file.name}, ID: #{file.id}, Location: #{file.location}"
92+
end
93+
94+
warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}"
95+
end
96+
97+
def auth_strategy = Registry["nextcloud.authentication.userless"].call
98+
99+
def managed_project_folder_ids
100+
@managed_project_folder_ids ||= ProjectStorage.automatic.where(storage: @storage)
101+
.pluck(:project_folder_id).to_set
102+
end
103+
104+
def files
105+
@files ||= Peripherals::Registry
106+
.resolve("#{@storage}.queries.files")
107+
.call(storage: @storage, auth_strategy:, folder: ParentFolder.new(@storage.group_folder))
108+
end
109+
end
110+
end
111+
end
112+
end
113+
end

modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/base_validator.rb

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ module Peripherals
3333
module ConnectionValidators
3434
module Nextcloud
3535
class BaseValidator
36+
include TaggedLogging
37+
3638
def initialize(storage)
3739
@storage = storage
3840
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
require "spec_helper"
32+
require_module_spec_helper
33+
34+
module Storages
35+
module Peripherals
36+
module ConnectionValidators
37+
module Nextcloud
38+
RSpec.describe AmpfConnectionValidator, :webmock do
39+
let(:storage) { create(:nextcloud_storage_configured, :as_automatically_managed) }
40+
let(:project_folder_id) { "1337" }
41+
let!(:project_storage) do
42+
create(:project_storage, :as_automatically_managed, project_folder_id:, storage:, project: create(:project))
43+
end
44+
45+
let(:files_response) do
46+
ServiceResult.success(result: StorageFiles.new(
47+
[StorageFile.new(id: project_folder_id, name: project_storage.managed_project_folder_name)],
48+
StorageFile.new(id: "root", name: "root"),
49+
[]
50+
))
51+
end
52+
53+
subject(:validator) { described_class.new(storage) }
54+
55+
before do
56+
Registry.stub("nextcloud.queries.files", ->(*) { files_response })
57+
end
58+
59+
it "pass all checks" do
60+
results = validator.call
61+
62+
expect(results.values).to all(be_success)
63+
end
64+
65+
context "if userless authentication fails" do
66+
let(:files_response) { build_failure(code: :unauthorized, payload: nil) }
67+
68+
it "fails and skips the next checks" do
69+
results = validator.call
70+
71+
states = results.values.map { it.state }.tally
72+
expect(states).to eq({ failure: 1, skipped: 3 })
73+
expect(results[:userless_access]).to be_failure
74+
expect(results[:userless_access].message).to eq(i18n_message(:userless_access_denied))
75+
end
76+
end
77+
78+
context "if the files request returns not_found" do
79+
let(:files_response) { build_failure(code: :not_found, payload: nil) }
80+
81+
it "fails the check" do
82+
results = validator.call
83+
84+
expect(results[:group_folder_presence]).to be_failure
85+
expect(results[:group_folder_presence].message).to eq(i18n_message(:group_folder_not_found))
86+
end
87+
end
88+
89+
context "if the files request returns an unknown error" do
90+
let(:files_response) { StorageInteraction::Nextcloud::Util.error(:error) }
91+
92+
before { allow(Rails.logger).to receive(:error) }
93+
94+
it "fails the check and logs the error" do
95+
results = validator.call
96+
97+
expect(results[:files_request]).to be_failure
98+
expect(results[:files_request].message)
99+
.to eq(i18n_message(:unknown_error))
100+
101+
expect(Rails.logger).to have_received(:error).with(/Connection validation failed with unknown error/)
102+
end
103+
end
104+
105+
context "if the files request returns unexpected files" do
106+
let(:files_response) do
107+
ServiceResult.success(result: StorageFiles.new(
108+
[
109+
StorageFile.new(id: project_folder_id, name: "I am your father"),
110+
StorageFile.new(id: "noooooooooo", name: "testimony_of_luke_skywalker.md")
111+
],
112+
StorageFile.new(id: "root", name: "root"),
113+
[]
114+
))
115+
end
116+
117+
it "warns the user about extraneous folders" do
118+
results = validator.call
119+
120+
expect(results[:group_folder_contents]).to be_a_warning
121+
expect(results[:group_folder_contents].message).to eq(i18n_message(:unexpected_content))
122+
end
123+
end
124+
125+
private
126+
127+
def i18n_message(key, context = {}) = I18n.t("storages.health.connection_validation.#{key}", **context)
128+
129+
def build_failure(code:, payload:)
130+
data = StorageErrorData.new(source: "query", payload:)
131+
error = StorageError.new(code:, data:)
132+
ServiceResult.failure(result: code, errors: error)
133+
end
134+
end
135+
end
136+
end
137+
end
138+
end

0 commit comments

Comments
 (0)