trusty-cms 7.1.2 → 7.2
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 +127 -111
- 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/string_extensions/string_extensions.rb +4 -2
- 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/assets_controller_spec.rb +124 -0
- data/spec/controllers/admin/changes_controller_spec.rb +72 -0
- data/spec/controllers/admin/configuration_controller_spec.rb +49 -0
- data/spec/controllers/admin/page_attachments_controller_spec.rb +54 -0
- data/spec/controllers/admin/pages_controller_spec.rb +162 -0
- data/spec/controllers/admin/preferences_controller_spec.rb +44 -0
- data/spec/controllers/admin/security_controller_spec.rb +85 -0
- data/spec/controllers/admin/sessions_controller_spec.rb +56 -0
- data/spec/controllers/admin/snippets_controller_spec.rb +95 -0
- data/spec/controllers/admin/two_factor_controller_spec.rb +63 -0
- data/spec/controllers/admin/users_controller_spec.rb +132 -0
- data/spec/controllers/page_status_controller_spec.rb +66 -0
- data/spec/controllers/site_controller_spec.rb +63 -0
- data/spec/dummy/bin/rails +3 -3
- data/spec/dummy/bin/rake +2 -2
- data/spec/dummy/bin/setup +21 -13
- data/spec/dummy/config/boot.rb +1 -1
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -2
- data/spec/dummy/config/initializers/inflections.rb +4 -4
- data/spec/dummy/config/initializers/new_framework_defaults_7_2.rb +70 -0
- data/spec/dummy/config/initializers/permissions_policy.rb +13 -0
- data/spec/dummy/config/puma.rb +34 -0
- data/spec/dummy/config/storage.yml +28 -1
- data/spec/dummy/db/migrate/20260729201444_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
- data/spec/dummy/db/migrate/20260729201445_create_active_storage_variant_records.active_storage.rb +27 -0
- data/spec/dummy/db/migrate/20260729201446_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
- data/spec/dummy/public/404.html +6 -6
- data/spec/dummy/public/406-unsupported-browser.html +66 -0
- data/spec/dummy/public/422.html +6 -6
- data/spec/dummy/public/500.html +6 -6
- data/spec/dummy/public/icon.png +0 -0
- data/spec/dummy/public/icon.svg +3 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/factories/site.rb +8 -0
- data/spec/helpers/admin/configuration_helper_spec.rb +62 -0
- data/spec/helpers/admin/node_helper_spec.rb +132 -0
- data/spec/helpers/admin/pages_helper_spec.rb +84 -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 +217 -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 +89 -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/spec/support/active_record_encryption.rb +10 -0
- data/spec/support/asset_type_registry.rb +29 -0
- data/trusty_cms.gemspec +4 -4
- data/yarn.lock +273 -259
- metadata +157 -14
- data/spec/controllers/assets_controller.rb +0 -66
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::SecurityController — password changes and TOTP two-factor
|
|
4
|
+
# enable/disable/verify for the current user. Touches devise-two-factor and QR
|
|
5
|
+
# provisioning, both plausible upgrade casualties.
|
|
6
|
+
RSpec.describe Admin::SecurityController, type: :controller do
|
|
7
|
+
routes { TrustyCms::Application.routes }
|
|
8
|
+
|
|
9
|
+
let(:user) { create(:admin) }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
allow(controller).to receive(:authenticate_user!).and_return(true)
|
|
13
|
+
allow(controller).to receive(:current_user).and_return(user)
|
|
14
|
+
# sign_out/sign_in need Warden wiring we don't set up here; the auth side
|
|
15
|
+
# effect isn't what these specs are checking.
|
|
16
|
+
allow(controller).to receive(:sign_out)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe 'GET #show' do
|
|
20
|
+
it 'generates an OTP secret and QR provisioning data' do
|
|
21
|
+
expect(user.otp_secret).to be_blank
|
|
22
|
+
|
|
23
|
+
get :show
|
|
24
|
+
|
|
25
|
+
expect(response).to have_http_status(:ok)
|
|
26
|
+
expect(user.reload.otp_secret).to be_present
|
|
27
|
+
expect(controller.instance_variable_get(:@qr_png_data)).to start_with('data:image/png')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe 'GET #edit' do
|
|
32
|
+
it 'succeeds' do
|
|
33
|
+
get :edit
|
|
34
|
+
expect(response).to have_http_status(:ok)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe 'PUT #update' do
|
|
39
|
+
it 'updates the password, signs out, and redirects to sign-in' do
|
|
40
|
+
put :update, params: { user: { password: 'NewComplex1!', password_confirmation: 'NewComplex1!' } }
|
|
41
|
+
|
|
42
|
+
expect(controller).to have_received(:sign_out).with(user)
|
|
43
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 're-renders edit when the password change is invalid' do
|
|
47
|
+
put :update, params: { user: { password: 'x', password_confirmation: 'y' } }
|
|
48
|
+
|
|
49
|
+
expect(flash[:error]).to be_present
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'POST #verify_two_factor' do
|
|
54
|
+
it 'enables 2FA when the OTP is valid' do
|
|
55
|
+
allow(user).to receive(:validate_and_consume_otp!).with('123456').and_return(true)
|
|
56
|
+
|
|
57
|
+
post :verify_two_factor, params: { otp_attempt: '123456' }
|
|
58
|
+
|
|
59
|
+
expect(user.reload.otp_required_for_login).to be(true)
|
|
60
|
+
expect(response).to redirect_to(admin_security_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'reports an error when the OTP is invalid' do
|
|
64
|
+
allow(user).to receive(:validate_and_consume_otp!).and_return(false)
|
|
65
|
+
|
|
66
|
+
post :verify_two_factor, params: { otp_attempt: 'bad' }
|
|
67
|
+
|
|
68
|
+
expect(flash[:error]).to be_present
|
|
69
|
+
expect(response).to redirect_to(admin_security_path)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe 'POST #disable_two_factor' do
|
|
74
|
+
it 'clears the OTP settings and redirects' do
|
|
75
|
+
user.update!(otp_required_for_login: true, otp_secret: User.generate_otp_secret)
|
|
76
|
+
|
|
77
|
+
post :disable_two_factor
|
|
78
|
+
|
|
79
|
+
user.reload
|
|
80
|
+
expect(user.otp_required_for_login).to be(false)
|
|
81
|
+
expect(user.otp_secret).to be_nil
|
|
82
|
+
expect(response).to redirect_to(admin_security_path)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::SessionsController#create — the custom login flow that
|
|
4
|
+
# branches into the two-factor handoff for OTP-enabled users. Overriding a
|
|
5
|
+
# Devise controller is exactly the sort of thing that can break across a Devise
|
|
6
|
+
# major, so pin the three branches (2FA, plain sign-in, bad credentials).
|
|
7
|
+
RSpec.describe Admin::SessionsController, type: :controller do
|
|
8
|
+
routes { TrustyCms::Application.routes }
|
|
9
|
+
include Devise::Test::ControllerHelpers
|
|
10
|
+
|
|
11
|
+
let(:password) { 'ComplexPass1!' }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
request.env['devise.mapping'] = Devise.mappings[:user]
|
|
15
|
+
# sign_in needs Warden wiring that isn't the focus here.
|
|
16
|
+
allow(controller).to receive(:sign_in)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe 'POST #create' do
|
|
20
|
+
it 'signs in a valid non-2FA user and redirects' do
|
|
21
|
+
user = create(:admin, password: password)
|
|
22
|
+
|
|
23
|
+
post :create, params: { user: { email: user.email, password: password } }
|
|
24
|
+
|
|
25
|
+
expect(controller).to have_received(:sign_in).with(:user, user)
|
|
26
|
+
expect(response).to redirect_to(admin_pages_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'starts the 2FA handoff for an OTP-enabled user' do
|
|
30
|
+
user = create(:admin, password: password, otp_required_for_login: true)
|
|
31
|
+
|
|
32
|
+
post :create, params: { user: { email: user.email, password: password } }
|
|
33
|
+
|
|
34
|
+
expect(response).to redirect_to(admin_two_factor_path)
|
|
35
|
+
expect(session[:pre_2fa_user_id]).to eq(user.id)
|
|
36
|
+
expect(session[:pre_2fa_started_at]).to be_present
|
|
37
|
+
expect(controller).not_to have_received(:sign_in)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 're-renders the sign-in form on bad credentials' do
|
|
41
|
+
user = create(:admin, password: password)
|
|
42
|
+
|
|
43
|
+
post :create, params: { user: { email: user.email, password: 'wrong' } }
|
|
44
|
+
|
|
45
|
+
expect(controller).not_to have_received(:sign_in)
|
|
46
|
+
expect(flash.now[:alert]).to be_present
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 're-renders the sign-in form for an unknown email' do
|
|
50
|
+
post :create, params: { user: { email: 'nobody@example.com', password: password } }
|
|
51
|
+
|
|
52
|
+
expect(controller).not_to have_received(:sign_in)
|
|
53
|
+
expect(flash.now[:alert]).to be_present
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises the full request cycle through Admin::ResourceController (the base
|
|
4
|
+
# for most admin CRUD): before-action chain, permitted_params, response_for
|
|
5
|
+
# dispatch, and the redirect/re-render paths. High blast radius for a Rails
|
|
6
|
+
# upgrade, so keep this green before and after the bump.
|
|
7
|
+
RSpec.describe Admin::SnippetsController, type: :controller do
|
|
8
|
+
routes { TrustyCms::Application.routes }
|
|
9
|
+
|
|
10
|
+
let(:user) { create(:admin) }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
allow(controller).to receive(:authenticate_user!).and_return(true)
|
|
14
|
+
allow(controller).to receive(:current_user).and_return(user)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe 'GET #index' do
|
|
18
|
+
it 'succeeds for an authorized user' do
|
|
19
|
+
create(:snippet, name: 'listed')
|
|
20
|
+
get :index
|
|
21
|
+
expect(response).to have_http_status(:ok)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'GET #new' do
|
|
26
|
+
it 'succeeds' do
|
|
27
|
+
get :new
|
|
28
|
+
expect(response).to have_http_status(:ok)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'POST #create' do
|
|
33
|
+
it 'creates a snippet and redirects to the index' do
|
|
34
|
+
expect {
|
|
35
|
+
post :create, params: { snippet: { name: 'greeting', content: 'Hello' } }
|
|
36
|
+
}.to change(Snippet, :count).by(1)
|
|
37
|
+
|
|
38
|
+
expect(response).to be_redirect
|
|
39
|
+
expect(Snippet.find_by(name: 'greeting').content).to eq('Hello')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'records the creating user' do
|
|
43
|
+
post :create, params: { snippet: { name: 'owned', content: 'x' } }
|
|
44
|
+
expect(Snippet.find_by(name: 'owned').created_by_id).to eq(user.id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# NOTE: Admin::ResourceController defines #rescue_action, but that is a
|
|
48
|
+
# Rails 2 hook the framework no longer calls, and it is not re-wired via
|
|
49
|
+
# rescue_from (only Admin::PagesController does that). So an invalid create
|
|
50
|
+
# currently raises RecordInvalid unhandled (a 500 in production) instead of
|
|
51
|
+
# re-rendering the form via `response_for :invalid`. Characterizing the
|
|
52
|
+
# current behavior; revisit if rescue_action is ever wired up.
|
|
53
|
+
it 'raises on invalid input and persists nothing (latent bug: rescue_action is dead)' do
|
|
54
|
+
expect {
|
|
55
|
+
post :create, params: { snippet: { name: '', content: 'x' } }
|
|
56
|
+
}.to raise_error(ActiveRecord::RecordInvalid)
|
|
57
|
+
|
|
58
|
+
expect(Snippet.where(name: '')).to be_empty
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe 'PUT #update' do
|
|
63
|
+
it 'updates the snippet and redirects' do
|
|
64
|
+
snippet = create(:snippet, name: 'editme', content: 'old')
|
|
65
|
+
|
|
66
|
+
put :update, params: { id: snippet.id, snippet: { content: 'new' } }
|
|
67
|
+
|
|
68
|
+
expect(response).to be_redirect
|
|
69
|
+
expect(snippet.reload.content).to eq('new')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe 'DELETE #destroy' do
|
|
74
|
+
it 'destroys the snippet and redirects' do
|
|
75
|
+
snippet = create(:snippet, name: 'goner')
|
|
76
|
+
|
|
77
|
+
expect {
|
|
78
|
+
delete :destroy, params: { id: snippet.id }
|
|
79
|
+
}.to change(Snippet, :count).by(-1)
|
|
80
|
+
|
|
81
|
+
expect(response).to be_redirect
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe 'authorization' do
|
|
86
|
+
it 'denies a user without editor privileges' do
|
|
87
|
+
allow(controller).to receive(:current_user).and_return(create(:user, email: 'plain@example.com'))
|
|
88
|
+
|
|
89
|
+
get :index
|
|
90
|
+
|
|
91
|
+
expect(response).to be_redirect
|
|
92
|
+
expect(flash[:error]).to be_present
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::TwoFactorController — the mid-login TOTP challenge. It runs
|
|
4
|
+
# BEFORE the user is fully signed in (authenticate_user! is skipped) and drives
|
|
5
|
+
# the flow off a short-lived session handoff (:pre_2fa_user_id / :started_at).
|
|
6
|
+
RSpec.describe Admin::TwoFactorController, type: :controller do
|
|
7
|
+
routes { TrustyCms::Application.routes }
|
|
8
|
+
# current_user is read via Devise even though authenticate_user! is skipped.
|
|
9
|
+
include Devise::Test::ControllerHelpers
|
|
10
|
+
|
|
11
|
+
let(:user) { create(:admin, otp_required_for_login: true) }
|
|
12
|
+
|
|
13
|
+
# Put the user mid-2FA: pending id in the session, started just now.
|
|
14
|
+
def begin_2fa!(started_at: Time.current.to_i)
|
|
15
|
+
session[:pre_2fa_user_id] = user.id
|
|
16
|
+
session[:pre_2fa_started_at] = started_at
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
before { allow(controller).to receive(:sign_in) }
|
|
20
|
+
|
|
21
|
+
describe 'GET #show' do
|
|
22
|
+
it 'renders the challenge for a user mid-2FA' do
|
|
23
|
+
begin_2fa!
|
|
24
|
+
get :show
|
|
25
|
+
expect(response).to have_http_status(:ok)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'redirects to sign-in when there is no pending 2FA session' do
|
|
29
|
+
get :show
|
|
30
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
31
|
+
expect(flash[:alert]).to be_present
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'redirects to sign-in when the 2FA session has expired' do
|
|
35
|
+
begin_2fa!(started_at: 10.minutes.ago.to_i)
|
|
36
|
+
get :show
|
|
37
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'POST #create' do
|
|
42
|
+
before { begin_2fa! }
|
|
43
|
+
|
|
44
|
+
it 'signs the user in and clears the handoff when the code is valid' do
|
|
45
|
+
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).with('123456').and_return(true)
|
|
46
|
+
|
|
47
|
+
post :create, params: { otp_attempt: '123456' }
|
|
48
|
+
|
|
49
|
+
expect(controller).to have_received(:sign_in).with(:user, an_instance_of(User))
|
|
50
|
+
expect(response).to redirect_to(admin_pages_path)
|
|
51
|
+
expect(session[:pre_2fa_user_id]).to be_nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'resets the session and redirects when the code is invalid' do
|
|
55
|
+
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_return(false)
|
|
56
|
+
|
|
57
|
+
post :create, params: { otp_attempt: 'wrong' }
|
|
58
|
+
|
|
59
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
60
|
+
expect(flash[:alert]).to be_present
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::UsersController, which layers admin-only authorization and
|
|
4
|
+
# several custom overrides (create/update/destroy/disable_2fa, plus the
|
|
5
|
+
# self-protection guards) on top of ResourceController. Devise/2FA touch points
|
|
6
|
+
# make this a useful regression net for a Rails/Ruby upgrade.
|
|
7
|
+
RSpec.describe Admin::UsersController, type: :controller do
|
|
8
|
+
routes { TrustyCms::Application.routes }
|
|
9
|
+
|
|
10
|
+
let(:admin) { create(:admin) }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
allow(controller).to receive(:authenticate_user!).and_return(true)
|
|
14
|
+
allow(controller).to receive(:current_user).and_return(admin)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid_user_params(overrides = {})
|
|
18
|
+
{
|
|
19
|
+
first_name: 'New',
|
|
20
|
+
last_name: 'Person',
|
|
21
|
+
email: 'newperson@example.com',
|
|
22
|
+
password: 'ComplexPass1!',
|
|
23
|
+
password_confirmation: 'ComplexPass1!',
|
|
24
|
+
}.merge(overrides)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe 'GET #index' do
|
|
28
|
+
it 'succeeds for an admin' do
|
|
29
|
+
get :index
|
|
30
|
+
expect(response).to have_http_status(:ok)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'GET #new' do
|
|
35
|
+
it 'succeeds' do
|
|
36
|
+
get :new
|
|
37
|
+
expect(response).to have_http_status(:ok)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'GET #show' do
|
|
42
|
+
it 'redirects to the edit page' do
|
|
43
|
+
user = create(:user, email: 'showme@example.com')
|
|
44
|
+
get :show, params: { id: user.id }
|
|
45
|
+
expect(response).to redirect_to(edit_admin_user_path(user.id))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe 'POST #create' do
|
|
50
|
+
it 'creates a user and redirects with a notice' do
|
|
51
|
+
expect {
|
|
52
|
+
post :create, params: { user: valid_user_params }
|
|
53
|
+
}.to change(User, :count).by(1)
|
|
54
|
+
|
|
55
|
+
expect(response).to redirect_to(admin_users_path)
|
|
56
|
+
expect(flash[:notice]).to eq('User was created.')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 're-renders new with an error when the user is invalid' do
|
|
60
|
+
expect {
|
|
61
|
+
post :create, params: { user: valid_user_params(email: '') }
|
|
62
|
+
}.not_to change(User, :count)
|
|
63
|
+
|
|
64
|
+
expect(flash[:error]).to match(/error saving the user/)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe 'PUT #update' do
|
|
69
|
+
it 'updates an existing user and redirects' do
|
|
70
|
+
user = create(:user, email: 'editme@example.com')
|
|
71
|
+
|
|
72
|
+
put :update, params: { id: user.id, user: { first_name: 'Renamed' } }
|
|
73
|
+
|
|
74
|
+
expect(response).to be_redirect
|
|
75
|
+
expect(user.reload.first_name).to eq('Renamed')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# NOTE: latent bug — the guard compares `user_params['admin'] == false`, but
|
|
79
|
+
# over a real HTTP request the checkbox param arrives as the string 'false',
|
|
80
|
+
# which is != false. So through the normal request path an admin CAN demote
|
|
81
|
+
# themselves and no warning is shown. Characterizing current behavior;
|
|
82
|
+
# revisit if the guard is ever fixed to coerce the param.
|
|
83
|
+
it 'does NOT block self-demotion when admin arrives as the string "false" (bug)' do
|
|
84
|
+
put :update, params: { id: admin.id, user: { admin: 'false' } }
|
|
85
|
+
|
|
86
|
+
expect(admin.reload.admin).to be(false)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe 'DELETE #destroy' do
|
|
91
|
+
it 'destroys another user' do
|
|
92
|
+
user = create(:user, email: 'goner@example.com')
|
|
93
|
+
|
|
94
|
+
expect {
|
|
95
|
+
delete :destroy, params: { id: user.id }
|
|
96
|
+
}.to change(User, :count).by(-1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'refuses to let a user delete themselves' do
|
|
100
|
+
expect {
|
|
101
|
+
delete :destroy, params: { id: admin.id }
|
|
102
|
+
}.not_to change(User, :count)
|
|
103
|
+
|
|
104
|
+
expect(response).to redirect_to(admin_users_path)
|
|
105
|
+
expect(flash[:error]).to be_present
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe 'PATCH #disable_2fa' do
|
|
110
|
+
it 'clears the OTP settings and redirects' do
|
|
111
|
+
user = create(:user, email: '2fa@example.com', otp_required_for_login: true)
|
|
112
|
+
|
|
113
|
+
patch :disable_2fa, params: { id: user.id }
|
|
114
|
+
|
|
115
|
+
user.reload
|
|
116
|
+
expect(user.otp_required_for_login).to be(false)
|
|
117
|
+
expect(user.otp_secret).to be_nil
|
|
118
|
+
expect(response).to redirect_to(admin_users_path)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe 'authorization' do
|
|
123
|
+
it 'denies a non-admin user' do
|
|
124
|
+
allow(controller).to receive(:current_user).and_return(create(:non_admin, email: 'plain@example.com'))
|
|
125
|
+
|
|
126
|
+
get :index
|
|
127
|
+
|
|
128
|
+
expect(response).to be_redirect
|
|
129
|
+
expect(flash[:error]).to be_present
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises PageStatusController#refresh — a token-authenticated, no-CSRF JSON
|
|
4
|
+
# endpoint that publishes scheduled pages whose time has come. Auth is a bearer
|
|
5
|
+
# token compared with secure_compare; cover the reject paths and the publish
|
|
6
|
+
# behaviour.
|
|
7
|
+
RSpec.describe PageStatusController, type: :controller do
|
|
8
|
+
routes { TrustyCms::Application.routes }
|
|
9
|
+
# refresh skips authenticate_user!, but ApplicationController before-actions
|
|
10
|
+
# still read current_user, which needs Warden injected on the request.
|
|
11
|
+
include Devise::Test::ControllerHelpers
|
|
12
|
+
|
|
13
|
+
let(:token) { 'secret-bearer-token' }
|
|
14
|
+
|
|
15
|
+
def authorize!(value = token)
|
|
16
|
+
request.headers['Authorization'] = "Bearer #{value}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
before do
|
|
20
|
+
allow(Rails.application.credentials).to receive(:dig)
|
|
21
|
+
.with(:trusty_cms, :page_status_bearer_token).and_return(token)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'POST #refresh authentication' do
|
|
25
|
+
it 'returns 401 when no token is provided' do
|
|
26
|
+
post :refresh
|
|
27
|
+
expect(response).to have_http_status(:unauthorized)
|
|
28
|
+
expect(JSON.parse(response.body)['error']).to eq('Missing Bearer Token')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'returns 401 when the token is wrong' do
|
|
32
|
+
authorize!('nope')
|
|
33
|
+
post :refresh
|
|
34
|
+
expect(response).to have_http_status(:unauthorized)
|
|
35
|
+
expect(JSON.parse(response.body)['error']).to eq('Invalid Bearer Token')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'POST #refresh' do
|
|
40
|
+
before { authorize! }
|
|
41
|
+
|
|
42
|
+
it 'publishes scheduled pages whose publish time has passed' do
|
|
43
|
+
due = create(:page, title: 'Due', slug: 'due', status_id: Status[:scheduled].id,
|
|
44
|
+
published_at: 1.day.ago)
|
|
45
|
+
|
|
46
|
+
post :refresh
|
|
47
|
+
|
|
48
|
+
expect(response).to have_http_status(:ok)
|
|
49
|
+
body = JSON.parse(response.body)
|
|
50
|
+
expect(body['updated_page_ids']).to include(due.id)
|
|
51
|
+
expect(due.reload.status_id).to eq(Status[:published].id)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'leaves future-scheduled pages alone' do
|
|
55
|
+
future = create(:page, title: 'Future', slug: 'future', status_id: Status[:scheduled].id,
|
|
56
|
+
published_at: 1.day.from_now)
|
|
57
|
+
|
|
58
|
+
post :refresh
|
|
59
|
+
|
|
60
|
+
body = JSON.parse(response.body)
|
|
61
|
+
expect(body['remaining_scheduled_page_ids']).to include(future.id)
|
|
62
|
+
expect(body['message']).to match(/No scheduled pages/)
|
|
63
|
+
expect(future.reload.status_id).to eq(Status[:scheduled].id)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe SiteController, type: :request do
|
|
4
|
+
let!(:home) { FactoryBot.create(:home, title: 'Home') }
|
|
5
|
+
|
|
6
|
+
before { home.parts.create(name: 'body', content: 'Welcome <r:title />') }
|
|
7
|
+
|
|
8
|
+
describe 'serving pages' do
|
|
9
|
+
it 'renders the home page at the root path' do
|
|
10
|
+
get '/'
|
|
11
|
+
expect(response).to have_http_status(:ok)
|
|
12
|
+
expect(response.body).to include('Welcome Home')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'renders a published child page by its path' do
|
|
16
|
+
about = FactoryBot.create(:page, title: 'About', slug: 'about', parent: home, status_id: Status[:published].id)
|
|
17
|
+
about.parts.create(name: 'body', content: 'About <r:title />')
|
|
18
|
+
|
|
19
|
+
get '/about'
|
|
20
|
+
expect(response).to have_http_status(:ok)
|
|
21
|
+
expect(response.body).to include('About About')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'renders a nested published page' do
|
|
25
|
+
section = FactoryBot.create(:page, title: 'Section', slug: 'section', parent: home, status_id: Status[:published].id)
|
|
26
|
+
article = FactoryBot.create(:page, title: 'Article', slug: 'article', parent: section, status_id: Status[:published].id)
|
|
27
|
+
article.parts.create(name: 'body', content: 'Deep content')
|
|
28
|
+
|
|
29
|
+
get '/section/article'
|
|
30
|
+
expect(response).to have_http_status(:ok)
|
|
31
|
+
expect(response.body).to include('Deep content')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe 'unknown paths' do
|
|
36
|
+
it 'renders the not-found template with a 404 status' do
|
|
37
|
+
get '/does/not/exist'
|
|
38
|
+
expect(response).to have_http_status(:not_found)
|
|
39
|
+
expect(response.body).to include('could not be located')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'content type' do
|
|
44
|
+
it 'sets the response Content-Type from the page layout' do
|
|
45
|
+
layout = Layout.create!(name: 'Plain', content: '<r:content />', content_type: 'text/plain')
|
|
46
|
+
page = FactoryBot.create(:page, title: 'Raw', slug: 'raw', parent: home, status_id: Status[:published].id, layout: layout)
|
|
47
|
+
page.parts.create(name: 'body', content: 'plain body')
|
|
48
|
+
|
|
49
|
+
get '/raw'
|
|
50
|
+
expect(response.media_type).to eq('text/plain')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '.cache_timeout' do
|
|
55
|
+
it 'round-trips through the response cache director' do
|
|
56
|
+
original = SiteController.cache_timeout
|
|
57
|
+
SiteController.cache_timeout = 5.minutes
|
|
58
|
+
expect(SiteController.cache_timeout).to eq(5.minutes)
|
|
59
|
+
ensure
|
|
60
|
+
SiteController.cache_timeout = original
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/spec/dummy/bin/rails
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
APP_PATH = File.expand_path(
|
|
3
|
-
require_relative
|
|
4
|
-
require
|
|
2
|
+
APP_PATH = File.expand_path("../config/application", __dir__)
|
|
3
|
+
require_relative "../config/boot"
|
|
4
|
+
require "rails/commands"
|
data/spec/dummy/bin/rake
CHANGED
data/spec/dummy/bin/setup
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
require
|
|
2
|
+
require "fileutils"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
APP_ROOT = File.expand_path("..", __dir__)
|
|
5
|
+
APP_NAME = "trusty-cms"
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
def system!(*args)
|
|
8
|
+
system(*args, exception: true)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
FileUtils.chdir APP_ROOT do
|
|
12
|
+
# This script is a way to set up or update your development environment automatically.
|
|
13
|
+
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
|
|
14
|
+
# Add necessary setup steps to this file.
|
|
10
15
|
|
|
11
16
|
puts "== Installing dependencies =="
|
|
12
|
-
system "gem install bundler --conservative"
|
|
13
|
-
system
|
|
17
|
+
system! "gem install bundler --conservative"
|
|
18
|
+
system("bundle check") || system!("bundle install")
|
|
14
19
|
|
|
15
20
|
# puts "\n== Copying sample files =="
|
|
16
21
|
# unless File.exist?("config/database.yml")
|
|
17
|
-
#
|
|
22
|
+
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
|
|
18
23
|
# end
|
|
19
24
|
|
|
20
25
|
puts "\n== Preparing database =="
|
|
21
|
-
system "bin/
|
|
26
|
+
system! "bin/rails db:prepare"
|
|
22
27
|
|
|
23
28
|
puts "\n== Removing old logs and tempfiles =="
|
|
24
|
-
system "
|
|
25
|
-
system "rm -rf tmp/cache"
|
|
29
|
+
system! "bin/rails log:clear tmp:clear"
|
|
26
30
|
|
|
27
31
|
puts "\n== Restarting application server =="
|
|
28
|
-
system "
|
|
32
|
+
system! "bin/rails restart"
|
|
33
|
+
|
|
34
|
+
# puts "\n== Configuring puma-dev =="
|
|
35
|
+
# system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
|
|
36
|
+
# system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
|
|
29
37
|
end
|
data/spec/dummy/config/boot.rb
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
|
|
3
3
|
|
|
4
4
|
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
|
5
|
-
#$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
|
|
5
|
+
#$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
# Define an application-wide content security policy.
|
|
4
|
+
# See the Securing Rails Applications Guide for more information:
|
|
5
|
+
# https://guides.rubyonrails.org/security.html#content-security-policy-header
|
|
6
|
+
|
|
7
|
+
# Rails.application.configure do
|
|
8
|
+
# config.content_security_policy do |policy|
|
|
9
|
+
# policy.default_src :self, :https
|
|
10
|
+
# policy.font_src :self, :https, :data
|
|
11
|
+
# policy.img_src :self, :https, :data
|
|
12
|
+
# policy.object_src :none
|
|
13
|
+
# policy.script_src :self, :https
|
|
14
|
+
# policy.style_src :self, :https
|
|
15
|
+
# # Specify URI for violation reports
|
|
16
|
+
# # policy.report_uri "/csp-violation-report-endpoint"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
|
|
20
|
+
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
|
21
|
+
# config.content_security_policy_nonce_directives = %w(script-src style-src)
|
|
22
|
+
#
|
|
23
|
+
# # Report violations without enforcing the policy.
|
|
24
|
+
# # config.content_security_policy_report_only = true
|
|
25
|
+
# end
|