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,84 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::PagesHelper — small presentation helpers used by the page
|
|
4
|
+
# editor (class/filter lookups, meta-error detection, description cleanup,
|
|
5
|
+
# parent-page select options, and the revert confirmation copy).
|
|
6
|
+
describe Admin::PagesHelper, type: :helper do
|
|
7
|
+
describe '#class_of_page' do
|
|
8
|
+
it 'returns the assigned page class' do
|
|
9
|
+
helper.instance_variable_set(:@page, Page.new)
|
|
10
|
+
expect(helper.class_of_page).to eq(Page)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#filter' do
|
|
15
|
+
it "returns the first part's filter when parts are present" do
|
|
16
|
+
part = double('PagePart', filter: 'markdown')
|
|
17
|
+
helper.instance_variable_set(:@page, double('Page', parts: [part]))
|
|
18
|
+
expect(helper.filter).to eq('markdown')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns nil when there are no parts' do
|
|
22
|
+
helper.instance_variable_set(:@page, double('Page', parts: []))
|
|
23
|
+
expect(helper.filter).to be_nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#meta_errors?' do
|
|
28
|
+
# Admin::PagesHelper#meta_errors? is shadowed by ApplicationHelper#meta_errors?
|
|
29
|
+
# in the view ancestry, so `helper.meta_errors?` would call the wrong one.
|
|
30
|
+
# Bind this module's implementation to the helper to exercise it directly.
|
|
31
|
+
def meta_errors?
|
|
32
|
+
Admin::PagesHelper.instance_method(:meta_errors?).bind(helper).call
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'is true when slug has an error' do
|
|
36
|
+
page = Page.new
|
|
37
|
+
page.errors.add(:slug, 'is invalid')
|
|
38
|
+
helper.instance_variable_set(:@page, page)
|
|
39
|
+
expect(meta_errors?).to be(true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# NOTE: latent bug — errors[:attr] returns an array, and an empty array is
|
|
43
|
+
# truthy, so `!!(errors[:slug] or errors[:breadcrumb])` is true even with no
|
|
44
|
+
# meta errors. This implementation always returns true; characterizing it.
|
|
45
|
+
it 'also (incorrectly) returns true when there are no meta errors' do
|
|
46
|
+
helper.instance_variable_set(:@page, Page.new)
|
|
47
|
+
expect(meta_errors?).to be(true)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '#clean_page_description' do
|
|
52
|
+
it 'strips leading/trailing whitespace and collapses internal runs' do
|
|
53
|
+
page = double('Page', description: ' hello world ')
|
|
54
|
+
expect(helper.clean_page_description(page)).to eq('hello world')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'removes tab characters entirely' do
|
|
58
|
+
page = double('Page', description: "tab\there")
|
|
59
|
+
expect(helper.clean_page_description(page)).to eq('tabhere')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#parent_page_options' do
|
|
64
|
+
it 'builds options_for_select from the available parent pages' do
|
|
65
|
+
home = FactoryBot.create(:home, title: 'Home')
|
|
66
|
+
child = FactoryBot.create(:page, title: 'Child', slug: 'child', parent: home)
|
|
67
|
+
current_site = double('Site', homepage_id: home.id)
|
|
68
|
+
allow(Page).to receive(:parent_pages).with(home.id).and_return([home])
|
|
69
|
+
|
|
70
|
+
html = helper.parent_page_options(current_site, child)
|
|
71
|
+
|
|
72
|
+
expect(html).to include('Home')
|
|
73
|
+
expect(html).to include("value=\"#{home.id}\"")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#revert_confirmation_message' do
|
|
78
|
+
it 'includes the version date and time' do
|
|
79
|
+
message = helper.revert_confirmation_message(update_date: 'July 29, 2026', update_time: '10:00 AM')
|
|
80
|
+
expect(message).to include('July 29, 2026')
|
|
81
|
+
expect(message).to include('10:00 AM')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Admin::ReferencesHelper, type: :helper do
|
|
4
|
+
describe '#class_of_page' do
|
|
5
|
+
it 'defaults to Page when no class_name param is present' do
|
|
6
|
+
allow(helper).to receive(:params).and_return({})
|
|
7
|
+
expect(helper.class_of_page).to eq(Page)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'constantizes the class_name param' do
|
|
11
|
+
allow(helper).to receive(:params).and_return(class_name: 'FileNotFoundPage')
|
|
12
|
+
expect(helper.class_of_page).to eq(FileNotFoundPage)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#filter' do
|
|
17
|
+
it 'is nil for an unknown filter name' do
|
|
18
|
+
allow(helper).to receive(:params).and_return(filter_name: 'No Such Filter')
|
|
19
|
+
expect(helper.filter).to be_nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'finds a text filter by its filter_name' do
|
|
23
|
+
HamlFilter # ensure the descendant is autoloaded
|
|
24
|
+
allow(helper).to receive(:params).and_return(filter_name: 'Haml')
|
|
25
|
+
expect(helper.filter).to eq(HamlFilter)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#filter_reference' do
|
|
30
|
+
it 'explains when there is no filter on the page part' do
|
|
31
|
+
allow(helper).to receive(:params).and_return({})
|
|
32
|
+
expect(helper.filter_reference).to eq('There is no filter on the current page part.')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'explains when a filter has no documentation' do
|
|
36
|
+
HamlFilter
|
|
37
|
+
allow(helper).to receive(:params).and_return(filter_name: 'Haml')
|
|
38
|
+
expect(helper.filter_reference).to eq('There is no documentation on this filter.')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#_display_name' do
|
|
43
|
+
it 'returns the page class display name for tags' do
|
|
44
|
+
allow(helper).to receive(:params).and_return(type: 'tags')
|
|
45
|
+
expect(helper._display_name).to eq(Page.display_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns the filter name for filters' do
|
|
49
|
+
HamlFilter
|
|
50
|
+
allow(helper).to receive(:params).and_return(type: 'filters', filter_name: 'Haml')
|
|
51
|
+
expect(helper._display_name).to eq('Haml')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Admin::UrlHelper, type: :helper do
|
|
4
|
+
describe '#format_path' do
|
|
5
|
+
it 'returns an empty string for a root or empty path' do
|
|
6
|
+
expect(helper.format_path('')).to eq('')
|
|
7
|
+
expect(helper.format_path('/')).to eq('')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'returns "Root" for a single segment' do
|
|
11
|
+
expect(helper.format_path('foo')).to eq('Root')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'returns "/" for two segments' do
|
|
15
|
+
expect(helper.format_path('foo/bar')).to eq('/')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'returns the middle subpath for deeper paths' do
|
|
19
|
+
expect(helper.format_path('foo/bar/baz')).to eq('/bar')
|
|
20
|
+
expect(helper.format_path('foo/bar/baz/qux')).to eq('/bar/baz')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#extract_base_url' do
|
|
25
|
+
it 'keeps the scheme and host for a normal url' do
|
|
26
|
+
expect(helper.extract_base_url('http://example.com/some/page')).to eq('http://example.com')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'preserves the port for localhost urls' do
|
|
30
|
+
expect(helper.extract_base_url('http://localhost:3000/some/page')).to eq('http://localhost:3000')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#default_route?' do
|
|
35
|
+
it 'is true for a plain Page' do
|
|
36
|
+
expect(helper.default_route?(Page.new)).to be(true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'is false for a page subclass with no configured route' do
|
|
40
|
+
expect(helper.default_route?(FileNotFoundPage.new)).to be_falsey
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#lookup_page_path' do
|
|
45
|
+
it 'returns nil when no custom routes are configured' do
|
|
46
|
+
expect(helper.lookup_page_path(FileNotFoundPage.new)).to be_nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#build_url' do
|
|
51
|
+
it 'appends the page path for default-route pages' do
|
|
52
|
+
page = Page.new
|
|
53
|
+
allow(page).to receive(:path).and_return('/about')
|
|
54
|
+
|
|
55
|
+
expect(helper.build_url('http://example.com', page)).to eq('http://example.com/about')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'returns nil for a non-default page with no configured path' do
|
|
59
|
+
expect(helper.build_url('http://example.com', FileNotFoundPage.new)).to be_nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#generate_page_url' do
|
|
64
|
+
it 'combines the base url and page path' do
|
|
65
|
+
page = Page.new
|
|
66
|
+
allow(page).to receive(:path).and_return('/about')
|
|
67
|
+
|
|
68
|
+
expect(helper.generate_page_url('http://example.com/current', page)).to eq('http://example.com/about')
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe ApplicationHelper, type: :helper do
|
|
4
|
+
describe 'title and subtitle' do
|
|
5
|
+
it 'falls back to the default title and subtitle' do
|
|
6
|
+
allow(helper).to receive(:trusty_config).and_return({})
|
|
7
|
+
|
|
8
|
+
expect(helper.title).to eq('Trusty CMS')
|
|
9
|
+
expect(helper.subtitle).to eq('Publishing for Small Teams')
|
|
10
|
+
expect(helper.default_page_title).to eq('Trusty CMS - Publishing for Small Teams')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'uses the configured title and subtitle when present' do
|
|
14
|
+
allow(helper).to receive(:trusty_config)
|
|
15
|
+
.and_return('admin.title' => 'My Admin', 'admin.subtitle' => 'My Subtitle')
|
|
16
|
+
|
|
17
|
+
expect(helper.title).to eq('My Admin')
|
|
18
|
+
expect(helper.subtitle).to eq('My Subtitle')
|
|
19
|
+
expect(helper.default_page_title).to eq('My Admin - My Subtitle')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#logged_in?' do
|
|
24
|
+
it 'is true when there is a current user' do
|
|
25
|
+
allow(helper).to receive(:current_user).and_return(User.new)
|
|
26
|
+
expect(helper.logged_in?).to be(true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'is false when there is no current user' do
|
|
30
|
+
allow(helper).to receive(:current_user).and_return(nil)
|
|
31
|
+
expect(helper.logged_in?).to be(false)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '#admin? and #designer?' do
|
|
36
|
+
it 'reflects an admin user' do
|
|
37
|
+
user = instance_double(User, admin?: true, designer?: false)
|
|
38
|
+
allow(helper).to receive(:current_user).and_return(user)
|
|
39
|
+
|
|
40
|
+
expect(helper.admin?).to be(true)
|
|
41
|
+
expect(helper.designer?).to be(true)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'treats a designer as a designer but not an admin' do
|
|
45
|
+
user = instance_double(User, admin?: false, designer?: true)
|
|
46
|
+
allow(helper).to receive(:current_user).and_return(user)
|
|
47
|
+
|
|
48
|
+
expect(helper.admin?).to be(false)
|
|
49
|
+
expect(helper.designer?).to be(true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'is false for both when there is no current user' do
|
|
53
|
+
allow(helper).to receive(:current_user).and_return(nil)
|
|
54
|
+
|
|
55
|
+
expect(helper.admin?).to be_falsey
|
|
56
|
+
expect(helper.designer?).to be_falsey
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#meta_errors? and #meta_label' do
|
|
61
|
+
it 'reports no meta errors and a "More" label' do
|
|
62
|
+
expect(helper.meta_errors?).to be(false)
|
|
63
|
+
expect(helper.meta_label).to eq('More')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '#translate_with_default' do
|
|
68
|
+
it 'returns the given name when there is no translation' do
|
|
69
|
+
expect(helper.translate_with_default('Some Untranslated Name')).to eq('Some Untranslated Name')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#clean' do
|
|
74
|
+
it 'collapses duplicate slashes and strips a trailing slash from the path' do
|
|
75
|
+
expect(helper.clean('http://example.com/a//b/')).to eq('/a/b')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#append_image_extension' do
|
|
80
|
+
it 'appends .png when no extension is present' do
|
|
81
|
+
expect(helper.send(:append_image_extension, 'admin/foo')).to eq('admin/foo.png')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'leaves an existing extension untouched' do
|
|
85
|
+
expect(helper.send(:append_image_extension, 'admin/foo.gif')).to eq('admin/foo.gif')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#available_locales_select' do
|
|
90
|
+
it 'prepends a default option with a blank value' do
|
|
91
|
+
expect(helper.available_locales_select.first.last).to eq('')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '#body_classes' do
|
|
96
|
+
it 'starts as an empty, memoized array' do
|
|
97
|
+
expect(helper.body_classes).to eq([])
|
|
98
|
+
helper.body_classes << 'reversed'
|
|
99
|
+
expect(helper.body_classes).to eq(['reversed'])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '#onsubmit_status' do
|
|
104
|
+
it 'reports a creating status for a new record' do
|
|
105
|
+
expect(helper.onsubmit_status(Snippet.new)).to match(/creat/i)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'reports a saving status for a persisted record' do
|
|
109
|
+
snippet = FactoryBot.create(:snippet, name: 'saved')
|
|
110
|
+
expect(helper.onsubmit_status(snippet)).to match(/saving/i)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#save_model_button' do
|
|
115
|
+
it 'labels the button Create for a new record' do
|
|
116
|
+
html = helper.save_model_button(Snippet.new)
|
|
117
|
+
expect(html).to include('type="submit"')
|
|
118
|
+
expect(html).to match(/Create/)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'labels the button Save Changes for a persisted record' do
|
|
122
|
+
snippet = FactoryBot.create(:snippet, name: 'persisted')
|
|
123
|
+
expect(helper.save_model_button(snippet)).to match(/Save Changes/)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe '#save_model_and_continue_editing_button' do
|
|
128
|
+
it 'renders a continue submit button' do
|
|
129
|
+
html = helper.save_model_and_continue_editing_button(Snippet.new)
|
|
130
|
+
expect(html).to include('name="continue"')
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe '#image' do
|
|
135
|
+
# Stub image_tag so the test exercises the path-building logic without doing
|
|
136
|
+
# a real asset-pipeline lookup (which warns for assets not in the pipeline).
|
|
137
|
+
before { allow(helper).to receive(:image_tag) { |path, _opts| path } }
|
|
138
|
+
|
|
139
|
+
it 'builds an admin image path, defaulting the extension to .png' do
|
|
140
|
+
helper.image('foo', alt: 'Foo')
|
|
141
|
+
expect(helper).to have_received(:image_tag).with('admin/foo.png', { alt: 'Foo' })
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'keeps an explicit extension' do
|
|
145
|
+
helper.image('bar.gif')
|
|
146
|
+
expect(helper).to have_received(:image_tag).with('admin/bar.gif', {})
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe '#timestamp' do
|
|
151
|
+
it 'localizes the time with the :timestamp format' do
|
|
152
|
+
time = Time.zone.local(2026, 7, 29, 10, 0, 0)
|
|
153
|
+
expect(helper.timestamp(time)).to eq(I18n.localize(time, format: :timestamp))
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe '#updated_stamp' do
|
|
158
|
+
it 'returns nil for a new record' do
|
|
159
|
+
expect(helper.updated_stamp(Snippet.new)).to be_nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'renders an updated line for a persisted record' do
|
|
163
|
+
snippet = FactoryBot.create(:snippet, name: 'stamped')
|
|
164
|
+
html = helper.updated_stamp(snippet)
|
|
165
|
+
expect(html).to include('updated_line')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
describe 'asset overrides' do
|
|
170
|
+
it 'returns no stylesheet overrides when the override files are absent' do
|
|
171
|
+
expect(helper.stylesheet_overrides).to eq([])
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'returns no javascript overrides when the override file is absent' do
|
|
175
|
+
expect(helper.javascript_overrides).to eq([])
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'includes the stylesheet override when the file exists' do
|
|
179
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
180
|
+
expect(helper.stylesheet_overrides).to eq(['admin/overrides'])
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'includes the javascript override when the file exists' do
|
|
184
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
185
|
+
expect(helper.javascript_overrides).to eq(['admin/overrides'])
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe '#current_url?' do
|
|
190
|
+
it 'matches the current request path' do
|
|
191
|
+
allow(helper.request).to receive(:original_fullpath).and_return('/admin/pages/1/edit')
|
|
192
|
+
expect(helper.current_url?('/admin/pages')).to be_truthy
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'does not match an unrelated path' do
|
|
196
|
+
allow(helper.request).to receive(:original_fullpath).and_return('/admin/snippets')
|
|
197
|
+
expect(helper.current_url?('/admin/pages')).to be_falsey
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
describe '#current_tab?' do
|
|
202
|
+
it 'is true for a tab containing the current url and memoizes it' do
|
|
203
|
+
item = double('NavItem', relative_url: '/admin/pages')
|
|
204
|
+
tab = [item]
|
|
205
|
+
allow(helper).to receive(:current_url?).with('/admin/pages').and_return(true)
|
|
206
|
+
|
|
207
|
+
expect(helper.current_tab?(tab)).to be(true)
|
|
208
|
+
expect(helper.instance_variable_get(:@current_tab)).to eq(tab)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
describe '#pagination_for' do
|
|
213
|
+
it 'returns nil for a collection that is not paginatable' do
|
|
214
|
+
expect(helper.pagination_for(%w[a b c])).to be_nil
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe ScopedHelper do
|
|
4
|
+
# ScopedHelper defines #title/#subtitle on its includer (via the included
|
|
5
|
+
# hook) and reads from #current_site, so mix it into a small host class and
|
|
6
|
+
# stub current_site rather than fight ApplicationHelper's own title/subtitle.
|
|
7
|
+
let(:host_class) do
|
|
8
|
+
Class.new do
|
|
9
|
+
include ScopedHelper
|
|
10
|
+
attr_accessor :site
|
|
11
|
+
def current_site
|
|
12
|
+
site
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
subject(:helper) { host_class.new }
|
|
18
|
+
|
|
19
|
+
def site_double(name:, subtitle:)
|
|
20
|
+
double('site', name: name, subtitle: subtitle)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#title' do
|
|
24
|
+
it 'uses the current site name when present' do
|
|
25
|
+
helper.site = site_double(name: 'My Site', subtitle: '')
|
|
26
|
+
expect(helper.title).to eq('My Site')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'falls back to the configured admin title when the site name is blank' do
|
|
30
|
+
allow(TrustyCms::Config).to receive(:[]).with('admin.title').and_return('Configured Title')
|
|
31
|
+
helper.site = site_double(name: '', subtitle: '')
|
|
32
|
+
expect(helper.title).to eq('Configured Title')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'falls back to the default when both the site name and config are blank' do
|
|
36
|
+
allow(TrustyCms::Config).to receive(:[]).with('admin.title').and_return(nil)
|
|
37
|
+
helper.site = site_double(name: '', subtitle: '')
|
|
38
|
+
expect(helper.title).to eq('TrustyCMS')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#subtitle' do
|
|
43
|
+
it 'uses the current site subtitle when present' do
|
|
44
|
+
helper.site = site_double(name: 'My Site', subtitle: 'My Subtitle')
|
|
45
|
+
expect(helper.subtitle).to eq('My Subtitle')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'falls back to the configured admin subtitle when the site subtitle is blank' do
|
|
49
|
+
allow(TrustyCms::Config).to receive(:[]).with('admin.subtitle').and_return('Configured Subtitle')
|
|
50
|
+
helper.site = site_double(name: 'My Site', subtitle: '')
|
|
51
|
+
expect(helper.subtitle).to eq('Configured Subtitle')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'falls back to the default when both the site subtitle and config are blank' do
|
|
55
|
+
allow(TrustyCms::Config).to receive(:[]).with('admin.subtitle').and_return(nil)
|
|
56
|
+
helper.site = site_double(name: 'My Site', subtitle: '')
|
|
57
|
+
expect(helper.subtitle).to eq('publishing for small teams')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# ActiveRecord::Base monkeypatches. object_id_attr in particular relies on
|
|
4
|
+
# .descendants and generated accessors, so pin its behaviour ahead of any
|
|
5
|
+
# Rails upgrade. Exercised through PagePart, which uses it in production.
|
|
6
|
+
describe 'ActiveRecord extensions' do
|
|
7
|
+
describe '.object_id_attr' do
|
|
8
|
+
it 'instantiates the matching descendant for the stored id' do
|
|
9
|
+
HamlFilter # ensure the descendant is autoloaded
|
|
10
|
+
part = PagePart.new(filter_id: 'Haml')
|
|
11
|
+
expect(part.filter).to be_a(HamlFilter)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'falls back to the base class when no descendant matches' do
|
|
15
|
+
part = PagePart.new(filter_id: nil)
|
|
16
|
+
expect(part.filter.class).to eq(TextFilter)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'memoises the built object until the id changes' do
|
|
20
|
+
part = PagePart.new(filter_id: nil)
|
|
21
|
+
first = part.filter
|
|
22
|
+
expect(part.filter).to equal(first)
|
|
23
|
+
|
|
24
|
+
HamlFilter
|
|
25
|
+
part.filter_id = 'Haml'
|
|
26
|
+
expect(part.filter).to be_a(HamlFilter)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '.validates_path' do
|
|
31
|
+
let(:model_class) do
|
|
32
|
+
Class.new(PageField) do
|
|
33
|
+
validates_path :content
|
|
34
|
+
def self.name
|
|
35
|
+
'PathValidatedField'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'is valid when the value resolves to a real page' do
|
|
41
|
+
page = FactoryBot.create(:page, slug: '/', status_id: Status[:published].id)
|
|
42
|
+
allow(Page).to receive(:find_by_path).and_return(page)
|
|
43
|
+
|
|
44
|
+
record = model_class.new(name: 'ref', content: '/')
|
|
45
|
+
record.valid?
|
|
46
|
+
expect(record.errors[:content]).to be_empty
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'adds an error when the path is not found' do
|
|
50
|
+
allow(Page).to receive(:find_by_path).and_return(nil)
|
|
51
|
+
|
|
52
|
+
record = model_class.new(name: 'ref', content: '/missing')
|
|
53
|
+
record.valid?
|
|
54
|
+
expect(record.errors[:content]).to be_present
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'adds an error when the path resolves to a FileNotFoundPage' do
|
|
58
|
+
allow(Page).to receive(:find_by_path).and_return(FileNotFoundPage.new)
|
|
59
|
+
|
|
60
|
+
record = model_class.new(name: 'ref', content: '/404')
|
|
61
|
+
record.valid?
|
|
62
|
+
expect(record.errors[:content]).to be_present
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# LoginSystem is TrustyCms's custom role-based authorization layer, mixed into
|
|
4
|
+
# ApplicationController. The class-level access rules are pure logic, so pin
|
|
5
|
+
# them ahead of any Rails upgrade without needing a full request cycle.
|
|
6
|
+
describe LoginSystem do
|
|
7
|
+
describe 'role-based access (.only_allow_access_to / .user_has_access_to_action?)' do
|
|
8
|
+
let(:controller_class) do
|
|
9
|
+
Class.new(ActionController::Base) do
|
|
10
|
+
include LoginSystem
|
|
11
|
+
only_allow_access_to :index, :show,
|
|
12
|
+
when: %i[admin designer],
|
|
13
|
+
denied_message: 'You need editor privileges.'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'records the declared permissions per action' do
|
|
18
|
+
perms = controller_class.controller_permissions[:index]
|
|
19
|
+
expect(perms[:when]).to eq(%i[admin designer])
|
|
20
|
+
expect(perms[:denied_message]).to eq('You need editor privileges.')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'grants access to a user with an allowed role' do
|
|
24
|
+
expect(controller_class.user_has_access_to_action?(User.new(admin: true), :index)).to be(true)
|
|
25
|
+
expect(controller_class.user_has_access_to_action?(User.new(designer: true), :show)).to be(true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'denies a user without an allowed role' do
|
|
29
|
+
expect(controller_class.user_has_access_to_action?(User.new, :index)).to be(false)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'denies an anonymous (nil) user' do
|
|
33
|
+
expect(controller_class.user_has_access_to_action?(nil, :index)).to be(false)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'allows any action that has no declared restriction' do
|
|
37
|
+
expect(controller_class.user_has_access_to_action?(User.new, :unlisted_action)).to be(true)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'conditional access (:if)' do
|
|
42
|
+
let(:controller_class) do
|
|
43
|
+
Class.new(ActionController::Base) do
|
|
44
|
+
include LoginSystem
|
|
45
|
+
only_allow_access_to :index, if: :allowed?
|
|
46
|
+
def allowed?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'delegates to the named predicate on the controller' do
|
|
53
|
+
expect(controller_class.user_has_access_to_action?(User.new, :index)).to be(true)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# NOTE: .login_required? / .login_required reference `filter_chain`, a Rails
|
|
58
|
+
# API removed years ago, and would raise NoMethodError if called. They are
|
|
59
|
+
# dead code today (only commented-out call sites remain), so they are not
|
|
60
|
+
# exercised here. Flagging for removal or repair rather than pinning.
|
|
61
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'ostruct'
|
|
3
|
+
|
|
4
|
+
# lib/ostruct.rb reopens OpenStruct (originally patched for a Ruby upgrade) and
|
|
5
|
+
# shadows the stdlib version via the load path. This is exactly the kind of
|
|
6
|
+
# patch a Ruby/Rails upgrade can break, so pin the behaviour we rely on.
|
|
7
|
+
describe 'OpenStruct patch' do
|
|
8
|
+
it 'is the project patch, not the stdlib implementation' do
|
|
9
|
+
expect(OpenStruct.instance_method(:initialize).source_location.first)
|
|
10
|
+
.to eq(File.expand_path('../../lib/ostruct.rb', __dir__))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'builds accessors from a hash, symbolizing string keys' do
|
|
14
|
+
os = OpenStruct.new('a' => 1, :b => 2)
|
|
15
|
+
expect(os.a).to eq(1)
|
|
16
|
+
expect(os.b).to eq(2)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns nil for an unset member' do
|
|
20
|
+
expect(OpenStruct.new.anything).to be_nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'assigns and reads a member dynamically' do
|
|
24
|
+
os = OpenStruct.new
|
|
25
|
+
os.foo = 'bar'
|
|
26
|
+
expect(os.foo).to eq('bar')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'raises ArgumentError when a setter is given the wrong arity' do
|
|
30
|
+
os = OpenStruct.new
|
|
31
|
+
expect { os.send(:foo=, 1, 2) }.to raise_error(ArgumentError)
|
|
32
|
+
end
|
|
33
|
+
end
|