trusty-cms 7.1.2 → 7.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +4 -3
- data/Gemfile.lock +58 -46
- data/README.md +2 -0
- data/app/controllers/admin/resource_controller.rb +1 -1
- data/app/controllers/application_controller.rb +15 -0
- data/app/models/asset.rb +29 -5
- data/app/models/standard_tags.rb +13 -7
- data/app/models/user.rb +13 -0
- data/lib/trusty_cms/site_scope_auth_reporter.rb +108 -0
- data/lib/trusty_cms/version.rb +1 -1
- data/package.json +1 -1
- data/spec/controllers/admin/snippets_controller_spec.rb +95 -0
- data/spec/controllers/site_controller_spec.rb +63 -0
- data/spec/factories/site.rb +8 -0
- data/spec/helpers/admin/references_helper_spec.rb +54 -0
- data/spec/helpers/admin/url_helper_spec.rb +71 -0
- data/spec/helpers/application_helper_spec.rb +102 -0
- data/spec/helpers/scoped_helper_spec.rb +60 -0
- data/spec/lib/active_record_extensions_spec.rb +65 -0
- data/spec/lib/login_system_spec.rb +61 -0
- data/spec/lib/ostruct_spec.rb +33 -0
- data/spec/lib/string_extensions_spec.rb +48 -0
- data/spec/lib/symbol_extensions_spec.rb +14 -0
- data/spec/lib/trusty_cms/config_cache_spec.rb +73 -0
- data/spec/models/admins_site_spec.rb +19 -0
- data/spec/models/asset_spec.rb +224 -0
- data/spec/models/asset_type_spec.rb +137 -0
- data/spec/models/file_not_found_page_spec.rb +33 -0
- data/spec/models/haml_filter_spec.rb +30 -0
- data/spec/models/menu_renderer_spec.rb +63 -0
- data/spec/models/page_attachment_spec.rb +29 -0
- data/spec/models/page_context_spec.rb +57 -0
- data/spec/models/page_field_spec.rb +15 -0
- data/spec/models/page_part_spec.rb +33 -0
- data/spec/models/page_spec.rb +184 -0
- data/spec/models/rails_page_spec.rb +32 -0
- data/spec/models/site_spec.rb +93 -0
- data/spec/models/snippet_finder_spec.rb +27 -0
- data/spec/models/snippet_tags_spec.rb +45 -0
- data/spec/models/standard_tags/children_spec.rb +159 -0
- data/spec/models/standard_tags/content_spec.rb +168 -0
- data/spec/models/standard_tags/meta_spec.rb +86 -0
- data/spec/models/standard_tags_spec.rb +32 -0
- data/spec/models/status_spec.rb +63 -0
- data/spec/models/text_filter_spec.rb +47 -0
- data/spec/models/trusty_cms/config_spec.rb +69 -0
- data/spec/models/trusty_cms/page_response_cache_director_spec.rb +72 -0
- data/spec/models/user_action_observer_spec.rb +39 -0
- data/spec/models/user_serialize_telemetry_spec.rb +124 -0
- data/spec/rails_helper.rb +13 -6
- data/spec/requests/admin/snippets_spec.rb +81 -0
- data/spec/spec_helper.rb +17 -3
- data/trusty_cms.gemspec +3 -3
- data/yarn.lock +273 -259
- metadata +91 -8
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Load the REAL multi_site scoping code (the extension is not activated in the
|
|
4
|
+
# dummy test app) so we reproduce the exact production site-scope on User.
|
|
5
|
+
require File.expand_path(
|
|
6
|
+
'../../vendor/extensions/multi-site-extension/lib/multi_site/scoped_model', __dir__
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# Telemetry for the intermittent CMS logout (issue #1040). This is observe-only:
|
|
10
|
+
# it must NOT change Devise's behavior, and must report exactly when a valid user
|
|
11
|
+
# is excluded from session deserialization by the site scope.
|
|
12
|
+
describe 'User.serialize_from_session telemetry (issue #1040)', type: :model do
|
|
13
|
+
# Apply the production multi_site User scope for this example group only, then
|
|
14
|
+
# restore it (config.order = "random", so a leaked default_scope would corrupt
|
|
15
|
+
# other specs).
|
|
16
|
+
around do |example|
|
|
17
|
+
original_default_scopes = User.default_scopes.dup
|
|
18
|
+
|
|
19
|
+
added_page_accessor = false
|
|
20
|
+
unless Page.respond_to?(:current_site)
|
|
21
|
+
Page.singleton_class.send(:attr_accessor, :current_site)
|
|
22
|
+
added_page_accessor = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless User.singleton_class.include?(MultiSite::ScopedModel::ScopedClassMethods)
|
|
26
|
+
User.extend(MultiSite::ScopedModel::ScopedClassMethods)
|
|
27
|
+
end
|
|
28
|
+
# `extend` above can't be undone, so pin is_site_scoped? explicitly per
|
|
29
|
+
# example (true here, false in ensure) to keep the telemetry from leaking on
|
|
30
|
+
# into unrelated specs after this group runs.
|
|
31
|
+
User.define_singleton_method(:is_site_scoped?) { true }
|
|
32
|
+
User.class_eval { default_scope { joins(user_scope_condition) } }
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
example.run
|
|
36
|
+
ensure
|
|
37
|
+
User.default_scopes = original_default_scopes
|
|
38
|
+
User.define_singleton_method(:is_site_scoped?) { false }
|
|
39
|
+
if added_page_accessor
|
|
40
|
+
Page.singleton_class.send(:remove_method, :current_site)
|
|
41
|
+
Page.singleton_class.send(:remove_method, :current_site=)
|
|
42
|
+
end
|
|
43
|
+
Thread.current[:trusty_request_host] = nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
let(:site_a) { create(:site) }
|
|
48
|
+
let(:site_b) { create(:site) }
|
|
49
|
+
|
|
50
|
+
# Editor assigned to site_a only.
|
|
51
|
+
let(:editor) do
|
|
52
|
+
user = create(:user, email: 'editor@example.com')
|
|
53
|
+
AdminsSite.create!(admin: user, site: site_a)
|
|
54
|
+
user
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def serialize
|
|
58
|
+
User.serialize_from_session(editor.id, editor.authenticatable_salt)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'when current_site excludes an otherwise-valid user (the bug)' do
|
|
62
|
+
before { Page.current_site = site_b }
|
|
63
|
+
|
|
64
|
+
it 'still returns nil — behavior is unchanged vs the scoped default' do
|
|
65
|
+
expect(serialize).to be_nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'reports the miss to the log with context and a classification' do
|
|
69
|
+
Thread.current[:trusty_request_host] = site_a.base_domain # host they belong to
|
|
70
|
+
expect(Rails.logger).to receive(:warn).with(/CMS site-scope logout/)
|
|
71
|
+
serialize
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'classifies a host-they-belong-to mismatch as a race' do
|
|
75
|
+
Thread.current[:trusty_request_host] = site_a.base_domain
|
|
76
|
+
ctx = TrustyCms::SiteScopeAuthReporter.build_context(editor)
|
|
77
|
+
expect(ctx[:classification]).to eq('race')
|
|
78
|
+
expect(ctx[:user_id]).to eq(editor.id)
|
|
79
|
+
expect(ctx[:current_site_id]).to eq(site_b.id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'classifies a host they are not assigned to as genuine_cross_site' do
|
|
83
|
+
Thread.current[:trusty_request_host] = site_b.base_domain # host they do NOT belong to
|
|
84
|
+
ctx = TrustyCms::SiteScopeAuthReporter.build_context(editor)
|
|
85
|
+
expect(ctx[:classification]).to eq('genuine_cross_site')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'classifies a request host that matches no site as host_unmatched' do
|
|
89
|
+
Thread.current[:trusty_request_host] = 'nomatch.invalid'
|
|
90
|
+
ctx = TrustyCms::SiteScopeAuthReporter.build_context(editor)
|
|
91
|
+
expect(ctx[:expected_site_id]).to be_nil
|
|
92
|
+
expect(ctx[:classification]).to eq('host_unmatched')
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'when current_site includes the user (normal case)' do
|
|
97
|
+
before { Page.current_site = site_a }
|
|
98
|
+
|
|
99
|
+
it 'returns the user and reports nothing' do
|
|
100
|
+
expect(Rails.logger).not_to receive(:warn).with(/CMS site-scope logout/)
|
|
101
|
+
expect(serialize).to eq(editor)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'when the session key maps to no user at all' do
|
|
106
|
+
before { Page.current_site = site_a }
|
|
107
|
+
|
|
108
|
+
it 'reports nothing (ordinary expired/invalid session)' do
|
|
109
|
+
expect(Rails.logger).not_to receive(:warn).with(/CMS site-scope logout/)
|
|
110
|
+
expect(User.serialize_from_session(0, 'nope')).to be_nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'never raises into the auth path if telemetry blows up' do
|
|
115
|
+
Page.current_site = site_b
|
|
116
|
+
allow(TrustyCms::SiteScopeAuthReporter).to receive(:build_context).and_raise('boom')
|
|
117
|
+
allow(Rails.logger).to receive(:error)
|
|
118
|
+
|
|
119
|
+
result = nil
|
|
120
|
+
expect { result = serialize }.not_to raise_error
|
|
121
|
+
expect(result).to be_nil
|
|
122
|
+
expect(Rails.logger).to have_received(:error).with(/SiteScopeAuthReporter error/)
|
|
123
|
+
end
|
|
124
|
+
end
|
data/spec/rails_helper.rb
CHANGED
|
@@ -3,12 +3,11 @@ ENV["RAILS_ENV"] ||= 'test'
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
require 'rspec/rails'
|
|
5
5
|
require 'capybara/rails'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
6
|
+
# JS-driven system/feature tests use headless Chrome via Selenium. Poltergeist
|
|
7
|
+
# (PhantomJS) was removed: it is unmaintained and does not run on modern
|
|
8
|
+
# Ruby/Rails. The driver is registered lazily by Capybara, so this line does
|
|
9
|
+
# not require selenium-webdriver unless a :js example actually runs.
|
|
10
|
+
Capybara.javascript_driver = :selenium_chrome_headless
|
|
12
11
|
|
|
13
12
|
require 'database_cleaner'
|
|
14
13
|
DatabaseCleaner.strategy = :truncation, {except: %w[config]}
|
|
@@ -46,6 +45,14 @@ RSpec.configure do |config|
|
|
|
46
45
|
# https://relishapp.com/rspec/rspec-rails/docs
|
|
47
46
|
config.infer_spec_type_from_file_location!
|
|
48
47
|
|
|
48
|
+
# Devise/Warden test helpers: sign_in for request specs, login_as for feature
|
|
49
|
+
# specs. (Previously the feature specs called login_as with no helper wired
|
|
50
|
+
# up.)
|
|
51
|
+
config.include Devise::Test::IntegrationHelpers, type: :request
|
|
52
|
+
config.include Warden::Test::Helpers, type: :feature
|
|
53
|
+
config.before(:each, type: :feature) { Warden.test_mode! }
|
|
54
|
+
config.after(:each, type: :feature) { Warden.test_reset! }
|
|
55
|
+
|
|
49
56
|
config.before(:suite) do
|
|
50
57
|
TrustyCms::Config.initialize_cache
|
|
51
58
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
# Full-stack smoke: routing -> Devise/Warden auth -> ResourceController ->
|
|
4
|
+
# responder -> JSON render -> model -> DB. Uses the JSON endpoints so the smoke
|
|
5
|
+
# doesn't depend on the (asset-pipeline-heavy) admin HTML layout, which can't be
|
|
6
|
+
# rendered in this test environment. This is a high-value regression net for a
|
|
7
|
+
# Rails/Ruby upgrade: it exercises the real request cycle end to end.
|
|
8
|
+
RSpec.describe 'Admin::Snippets requests', type: :request do
|
|
9
|
+
let(:admin) { FactoryBot.create(:admin) }
|
|
10
|
+
|
|
11
|
+
describe 'authentication' do
|
|
12
|
+
it 'redirects an unauthenticated HTML request to sign-in' do
|
|
13
|
+
get '/admin/snippets'
|
|
14
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'returns 401 for an unauthenticated JSON request' do
|
|
18
|
+
get '/admin/snippets.json'
|
|
19
|
+
expect(response).to have_http_status(:unauthorized)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'authorization' do
|
|
24
|
+
it 'forbids a signed-in user without editor privileges (JSON)' do
|
|
25
|
+
sign_in FactoryBot.create(:user, email: 'plain@example.com')
|
|
26
|
+
get '/admin/snippets.json'
|
|
27
|
+
expect(response).to have_http_status(:forbidden)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'when signed in as an admin' do
|
|
32
|
+
before { sign_in admin }
|
|
33
|
+
|
|
34
|
+
it 'lists snippets as JSON' do
|
|
35
|
+
FactoryBot.create(:snippet, name: 'listed', content: 'x')
|
|
36
|
+
|
|
37
|
+
get '/admin/snippets.json'
|
|
38
|
+
|
|
39
|
+
expect(response).to have_http_status(:ok)
|
|
40
|
+
names = JSON.parse(response.body).map { |s| s['name'] }
|
|
41
|
+
expect(names).to include('listed')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'creates a snippet via JSON' do
|
|
45
|
+
expect {
|
|
46
|
+
post '/admin/snippets.json', params: { snippet: { name: 'greeting', content: 'hi' } }
|
|
47
|
+
}.to change(Snippet, :count).by(1)
|
|
48
|
+
|
|
49
|
+
expect(response).to have_http_status(:created)
|
|
50
|
+
expect(Snippet.find_by(name: 'greeting').content).to eq('hi')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'updates a snippet via JSON' do
|
|
54
|
+
snippet = FactoryBot.create(:snippet, name: 'editme', content: 'old')
|
|
55
|
+
|
|
56
|
+
put "/admin/snippets/#{snippet.id}.json", params: { snippet: { content: 'new' } }
|
|
57
|
+
|
|
58
|
+
expect(response).to have_http_status(:ok)
|
|
59
|
+
expect(snippet.reload.content).to eq('new')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'destroys the record via JSON' do
|
|
63
|
+
snippet = FactoryBot.create(:snippet, name: 'goner')
|
|
64
|
+
|
|
65
|
+
expect {
|
|
66
|
+
delete "/admin/snippets/#{snippet.id}.json"
|
|
67
|
+
}.to change(Snippet, :count).by(-1)
|
|
68
|
+
|
|
69
|
+
expect(response).to have_http_status(:ok)
|
|
70
|
+
expect(response.body).to be_empty
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'destroys the record and redirects (HTML)' do
|
|
74
|
+
snippet = FactoryBot.create(:snippet, name: 'goner-html')
|
|
75
|
+
expect {
|
|
76
|
+
delete "/admin/snippets/#{snippet.id}"
|
|
77
|
+
}.to change(Snippet, :count).by(-1)
|
|
78
|
+
expect(response).to redirect_to(admin_snippets_path)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
ENV['RAILS_ENV'] ||= 'test'
|
|
2
|
-
require File.expand_path('../dummy/config/environment.rb', __FILE__)
|
|
3
2
|
|
|
4
|
-
# Coverage setup
|
|
3
|
+
# Coverage setup — MUST run before any application code is loaded,
|
|
4
|
+
# otherwise Ruby's Coverage library never sees files loaded at boot.
|
|
5
5
|
require 'simplecov'
|
|
6
6
|
require 'simplecov-lcov'
|
|
7
|
+
require 'simplecov_json_formatter'
|
|
7
8
|
|
|
8
9
|
SimpleCov::Formatter::LcovFormatter.config do |config|
|
|
9
10
|
config.report_with_single_file = true
|
|
@@ -11,9 +12,22 @@ SimpleCov::Formatter::LcovFormatter.config do |config|
|
|
|
11
12
|
config.lcov_file_name = 'lcov.info'
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
SimpleCov.
|
|
15
|
+
SimpleCov.configure do
|
|
16
|
+
add_filter %r{^/lib/generators/}
|
|
17
|
+
add_filter 'lib/trusty_cms/setup.rb'
|
|
18
|
+
add_filter %r{/templates/} # generated-app boilerplate
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
|
|
22
|
+
SimpleCov::Formatter::JSONFormatter, # coverage/coverage.json — consumed by qlty
|
|
23
|
+
SimpleCov::Formatter::LcovFormatter,
|
|
24
|
+
SimpleCov::Formatter::HTMLFormatter
|
|
25
|
+
])
|
|
15
26
|
SimpleCov.start('rails')
|
|
16
27
|
|
|
28
|
+
# Now load the application — everything required below is tracked.
|
|
29
|
+
require File.expand_path('../dummy/config/environment.rb', __FILE__)
|
|
30
|
+
|
|
17
31
|
# Test framework setup
|
|
18
32
|
require 'rspec/rails'
|
|
19
33
|
require 'factory_bot_rails'
|
data/trusty_cms.gemspec
CHANGED
|
@@ -46,15 +46,15 @@ a general purpose content management system--not merely a blogging engine.'
|
|
|
46
46
|
s.add_dependency 'mutex_m'
|
|
47
47
|
s.add_dependency 'mysql2'
|
|
48
48
|
s.add_dependency 'paper_trail', '~> 16.0.0'
|
|
49
|
-
s.add_dependency 'paper_trail-association_tracking', '
|
|
50
|
-
s.add_dependency 'psych', '5.
|
|
49
|
+
s.add_dependency 'paper_trail-association_tracking', '>= 2.2.1', '< 2.4.0'
|
|
50
|
+
s.add_dependency 'psych', '5.4.0'
|
|
51
51
|
s.add_dependency 'rack', '>= 2.0.1', '< 3.2.0'
|
|
52
52
|
s.add_dependency 'rack-cache', '~> 1.7'
|
|
53
53
|
s.add_dependency 'radius', '~> 0.8'
|
|
54
54
|
s.add_dependency 'rails', '~> 7.2.2.1'
|
|
55
55
|
s.add_dependency 'rake', '< 14.0'
|
|
56
56
|
s.add_dependency 'ransack', '~> 4.2.1'
|
|
57
|
-
s.add_dependency 'rdoc', '>= 5.1', '<
|
|
57
|
+
s.add_dependency 'rdoc', '>= 5.1', '< 9.0'
|
|
58
58
|
s.add_dependency 'RedCloth', '4.3.4'
|
|
59
59
|
s.add_dependency 'roadie-rails'
|
|
60
60
|
s.add_dependency 'rqrcode'
|