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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -3
  3. data/Gemfile.lock +58 -46
  4. data/README.md +2 -0
  5. data/app/controllers/admin/resource_controller.rb +1 -1
  6. data/app/controllers/application_controller.rb +15 -0
  7. data/app/models/asset.rb +29 -5
  8. data/app/models/standard_tags.rb +13 -7
  9. data/app/models/user.rb +13 -0
  10. data/lib/trusty_cms/site_scope_auth_reporter.rb +108 -0
  11. data/lib/trusty_cms/version.rb +1 -1
  12. data/package.json +1 -1
  13. data/spec/controllers/admin/snippets_controller_spec.rb +95 -0
  14. data/spec/controllers/site_controller_spec.rb +63 -0
  15. data/spec/factories/site.rb +8 -0
  16. data/spec/helpers/admin/references_helper_spec.rb +54 -0
  17. data/spec/helpers/admin/url_helper_spec.rb +71 -0
  18. data/spec/helpers/application_helper_spec.rb +102 -0
  19. data/spec/helpers/scoped_helper_spec.rb +60 -0
  20. data/spec/lib/active_record_extensions_spec.rb +65 -0
  21. data/spec/lib/login_system_spec.rb +61 -0
  22. data/spec/lib/ostruct_spec.rb +33 -0
  23. data/spec/lib/string_extensions_spec.rb +48 -0
  24. data/spec/lib/symbol_extensions_spec.rb +14 -0
  25. data/spec/lib/trusty_cms/config_cache_spec.rb +73 -0
  26. data/spec/models/admins_site_spec.rb +19 -0
  27. data/spec/models/asset_spec.rb +224 -0
  28. data/spec/models/asset_type_spec.rb +137 -0
  29. data/spec/models/file_not_found_page_spec.rb +33 -0
  30. data/spec/models/haml_filter_spec.rb +30 -0
  31. data/spec/models/menu_renderer_spec.rb +63 -0
  32. data/spec/models/page_attachment_spec.rb +29 -0
  33. data/spec/models/page_context_spec.rb +57 -0
  34. data/spec/models/page_field_spec.rb +15 -0
  35. data/spec/models/page_part_spec.rb +33 -0
  36. data/spec/models/page_spec.rb +184 -0
  37. data/spec/models/rails_page_spec.rb +32 -0
  38. data/spec/models/site_spec.rb +93 -0
  39. data/spec/models/snippet_finder_spec.rb +27 -0
  40. data/spec/models/snippet_tags_spec.rb +45 -0
  41. data/spec/models/standard_tags/children_spec.rb +159 -0
  42. data/spec/models/standard_tags/content_spec.rb +168 -0
  43. data/spec/models/standard_tags/meta_spec.rb +86 -0
  44. data/spec/models/standard_tags_spec.rb +32 -0
  45. data/spec/models/status_spec.rb +63 -0
  46. data/spec/models/text_filter_spec.rb +47 -0
  47. data/spec/models/trusty_cms/config_spec.rb +69 -0
  48. data/spec/models/trusty_cms/page_response_cache_director_spec.rb +72 -0
  49. data/spec/models/user_action_observer_spec.rb +39 -0
  50. data/spec/models/user_serialize_telemetry_spec.rb +124 -0
  51. data/spec/rails_helper.rb +13 -6
  52. data/spec/requests/admin/snippets_spec.rb +81 -0
  53. data/spec/spec_helper.rb +17 -3
  54. data/trusty_cms.gemspec +3 -3
  55. data/yarn.lock +273 -259
  56. 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
- require 'capybara/poltergeist'
7
- Capybara.javascript_driver = :poltergeist
8
-
9
- Capybara.register_driver :poltergeist do |app|
10
- Capybara::Poltergeist::Driver.new(app, timeout: 60)
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.formatter = SimpleCov::Formatter::LcovFormatter
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', '~> 2.2.1'
50
- s.add_dependency 'psych', '5.2.2'
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', '< 7.0'
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'