trusty-cms 7.1.3 → 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.lock +72 -68
- data/lib/string_extensions/string_extensions.rb +4 -2
- data/lib/trusty_cms/version.rb +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/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/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/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/application_helper_spec.rb +115 -0
- data/spec/lib/string_extensions_spec.rb +41 -0
- data/spec/support/active_record_encryption.rb +10 -0
- data/spec/support/asset_type_registry.rb +29 -0
- data/trusty_cms.gemspec +1 -1
- metadata +68 -8
- data/spec/controllers/assets_controller.rb +0 -66
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::PagesController — the most heavily customized ResourceController
|
|
4
|
+
# subclass (search, restore, per-site edit guards, meta-row setup, ordering).
|
|
5
|
+
# Controller specs (views not rendered) let us drive the actions and their many
|
|
6
|
+
# private helpers without the asset-pipeline-heavy admin HTML layout. This is the
|
|
7
|
+
# highest-blast-radius controller in the app, so keep it green across the Rails 8 bump.
|
|
8
|
+
RSpec.describe Admin::PagesController, type: :controller do
|
|
9
|
+
routes { TrustyCms::Application.routes }
|
|
10
|
+
|
|
11
|
+
let(:admin) { create(:admin) }
|
|
12
|
+
let!(:home) { create(:home, title: 'Home') }
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
allow(controller).to receive(:authenticate_user!).and_return(true)
|
|
16
|
+
allow(controller).to receive(:current_user).and_return(admin)
|
|
17
|
+
# #current_site / Page.current_site / Page.homepage come from the multi-site
|
|
18
|
+
# extension, which isn't loaded in the dummy app; stub the touch points.
|
|
19
|
+
allow(Page).to receive(:current_site).and_return(nil)
|
|
20
|
+
allow(Page).to receive(:homepage).and_return(home)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'GET #index' do
|
|
24
|
+
it 'succeeds and sets up the site/homepage/search' do
|
|
25
|
+
get :index
|
|
26
|
+
expect(response).to have_http_status(:ok)
|
|
27
|
+
expect(controller.instance_variable_get(:@homepage)).to eq(home)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe 'GET #new' do
|
|
32
|
+
it 'builds a new page with defaults' do
|
|
33
|
+
get :new
|
|
34
|
+
expect(response).to have_http_status(:ok)
|
|
35
|
+
page = controller.instance_variable_get(:@page)
|
|
36
|
+
expect(page).to be_a(Page).and be_new_record
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'roots a top-level page at "/" when no parent is given' do
|
|
40
|
+
get :new
|
|
41
|
+
expect(controller.instance_variable_get(:@page).slug).to eq('/')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'sets the parent_id from page_id' do
|
|
45
|
+
get :new, params: { page_id: home.id }
|
|
46
|
+
expect(controller.instance_variable_get(:@page).parent_id).to eq(home.id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe 'GET #edit' do
|
|
51
|
+
context 'when the page belongs to a site the admin can access' do
|
|
52
|
+
let(:site) { create(:site) }
|
|
53
|
+
let(:page) { create(:page, title: 'Editable', parent: home, site_id: site.id) }
|
|
54
|
+
|
|
55
|
+
before { admin.sites << site }
|
|
56
|
+
|
|
57
|
+
it 'succeeds and loads edit assigns' do
|
|
58
|
+
get :edit, params: { id: page.id }
|
|
59
|
+
|
|
60
|
+
expect(response).to have_http_status(:ok)
|
|
61
|
+
expect(controller.instance_variable_get(:@page)).to eq(page)
|
|
62
|
+
expect(controller.instance_variable_get(:@page_url)).to be_present
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'when the page belongs to a site the admin cannot access' do
|
|
67
|
+
let(:site) { create(:site) }
|
|
68
|
+
let(:page) { create(:page, title: 'Forbidden', parent: home, site_id: site.id) }
|
|
69
|
+
|
|
70
|
+
it 'redirects back to the pages index' do
|
|
71
|
+
get :edit, params: { id: page.id }
|
|
72
|
+
expect(response).to redirect_to(admin_pages_url)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe 'POST #create' do
|
|
78
|
+
it 'creates a page and redirects' do
|
|
79
|
+
expect {
|
|
80
|
+
post :create, params: { page: { title: 'Fresh', slug: 'fresh', breadcrumb: 'Fresh', parent_id: home.id } }
|
|
81
|
+
}.to change(Page, :count).by(1)
|
|
82
|
+
|
|
83
|
+
expect(response).to be_redirect
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 're-renders new via the rescue_from validation_error handler when invalid' do
|
|
87
|
+
expect {
|
|
88
|
+
post :create, params: { page: { title: '', slug: '', breadcrumb: '', parent_id: home.id } }
|
|
89
|
+
}.not_to change(Page, :count)
|
|
90
|
+
|
|
91
|
+
expect(flash[:error]).to be_present
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe 'PUT #update' do
|
|
96
|
+
it 'updates the page and redirects' do
|
|
97
|
+
page = create(:page, title: 'Before', parent: home)
|
|
98
|
+
|
|
99
|
+
put :update, params: { id: page.id, page: { title: 'After' } }
|
|
100
|
+
|
|
101
|
+
expect(response).to be_redirect
|
|
102
|
+
expect(page.reload.title).to eq('After')
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe 'DELETE #destroy' do
|
|
107
|
+
it 'destroys the page and counts descendants' do
|
|
108
|
+
page = create(:page, title: 'Doomed', parent: home)
|
|
109
|
+
|
|
110
|
+
expect {
|
|
111
|
+
delete :destroy, params: { id: page.id }
|
|
112
|
+
}.to change(Page, :count).by(-1)
|
|
113
|
+
|
|
114
|
+
expect(response).to be_redirect
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe 'GET #search' do
|
|
119
|
+
it 'returns matching pages when a query is present' do
|
|
120
|
+
create(:page, title: 'Findable', slug: 'findable', parent: home)
|
|
121
|
+
|
|
122
|
+
get :search, params: { site_id: nil, search: { query: 'Findable' } }
|
|
123
|
+
|
|
124
|
+
expect(response).to have_http_status(:ok)
|
|
125
|
+
titles = controller.instance_variable_get(:@pages).map(&:title)
|
|
126
|
+
expect(titles).to include('Findable')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'does not run a query when none is given' do
|
|
130
|
+
get :search, params: { site_id: '' }
|
|
131
|
+
expect(controller.instance_variable_get(:@pages)).to be_nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe 'POST #save_table_position' do
|
|
136
|
+
it 'saves the new order and returns ok' do
|
|
137
|
+
allow(Page).to receive(:save_order)
|
|
138
|
+
post :save_table_position, params: { new_position: %w[1 2 3] }
|
|
139
|
+
expect(response).to have_http_status(:ok)
|
|
140
|
+
expect(Page).to have_received(:save_order).with(%w[1 2 3])
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe 'PUT #restore' do
|
|
145
|
+
# NOTE: latent bug / Rails 8 upgrade blocker — restore_page_version calls
|
|
146
|
+
# PaperTrail's `reify`, which YAML-loads the stored version. Under Psych 4+
|
|
147
|
+
# (psych 5.4 is pinned) safe-loading rejects ActiveSupport::TimeWithZone
|
|
148
|
+
# because no permitted classes are configured, so restoring any page that has
|
|
149
|
+
# a timestamp in its versioned state raises Psych::DisallowedClass. Page
|
|
150
|
+
# restore is effectively broken until paper_trail's serializer is configured
|
|
151
|
+
# with permitted classes (e.g. via ActiveRecord::Base.yaml_column_permitted_classes
|
|
152
|
+
# or a custom serializer). Characterizing current behavior.
|
|
153
|
+
it 'currently fails to reify a versioned page (Psych safe-load blocker)' do
|
|
154
|
+
page = create(:page, title: 'V1', parent: home)
|
|
155
|
+
PaperTrail.request(whodunnit: admin.id.to_s) { page.update!(title: 'V2') }
|
|
156
|
+
|
|
157
|
+
expect {
|
|
158
|
+
put :restore, params: { id: page.id, version_index: 1 }
|
|
159
|
+
}.to raise_error(Psych::DisallowedClass, /TimeWithZone/)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::PreferencesController, which lets the current user edit their
|
|
4
|
+
# own profile/preferences. Simple ApplicationController subclass; covers the
|
|
5
|
+
# show/edit render paths and the valid/invalid update branches.
|
|
6
|
+
RSpec.describe Admin::PreferencesController, type: :controller do
|
|
7
|
+
routes { TrustyCms::Application.routes }
|
|
8
|
+
|
|
9
|
+
let(:admin) { 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(admin)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'GET #show' do
|
|
17
|
+
it 'succeeds' do
|
|
18
|
+
get :show
|
|
19
|
+
expect(response).to have_http_status(:ok)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'GET #edit' do
|
|
24
|
+
it 'succeeds' do
|
|
25
|
+
get :edit
|
|
26
|
+
expect(response).to have_http_status(:ok)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'PUT #update' do
|
|
31
|
+
it 'updates the current user and redirects to configuration' do
|
|
32
|
+
put :update, params: { user: { first_name: 'Preferred' } }
|
|
33
|
+
|
|
34
|
+
expect(response).to redirect_to(admin_configuration_path)
|
|
35
|
+
expect(admin.reload.first_name).to eq('Preferred')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 're-renders edit with an error when the update is invalid' do
|
|
39
|
+
put :update, params: { user: { email: 'not-an-email' } }
|
|
40
|
+
|
|
41
|
+
expect(flash[:error]).to be_present
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -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,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
|
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__)
|