trusty-cms 7.1.3 → 8.0
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 +118 -123
- data/config/application.rb +5 -1
- 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/application.rb +2 -0
- 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 +2 -1
- metadata +74 -6
- data/spec/controllers/assets_controller.rb +0 -66
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Your browser is not supported (406)</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
.rails-default-error-page {
|
|
8
|
+
background-color: #EFEFEF;
|
|
9
|
+
color: #2E2F30;
|
|
10
|
+
text-align: center;
|
|
11
|
+
font-family: arial, sans-serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.rails-default-error-page div.dialog {
|
|
16
|
+
width: 95%;
|
|
17
|
+
max-width: 33em;
|
|
18
|
+
margin: 4em auto 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.rails-default-error-page div.dialog > div {
|
|
22
|
+
border: 1px solid #CCC;
|
|
23
|
+
border-right-color: #999;
|
|
24
|
+
border-left-color: #999;
|
|
25
|
+
border-bottom-color: #BBB;
|
|
26
|
+
border-top: #B00100 solid 4px;
|
|
27
|
+
border-top-left-radius: 9px;
|
|
28
|
+
border-top-right-radius: 9px;
|
|
29
|
+
background-color: white;
|
|
30
|
+
padding: 7px 12% 0;
|
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.rails-default-error-page h1 {
|
|
35
|
+
font-size: 100%;
|
|
36
|
+
color: #730E15;
|
|
37
|
+
line-height: 1.5em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.rails-default-error-page div.dialog > p {
|
|
41
|
+
margin: 0 0 1em;
|
|
42
|
+
padding: 1em;
|
|
43
|
+
background-color: #F7F7F7;
|
|
44
|
+
border: 1px solid #CCC;
|
|
45
|
+
border-right-color: #999;
|
|
46
|
+
border-left-color: #999;
|
|
47
|
+
border-bottom-color: #999;
|
|
48
|
+
border-bottom-left-radius: 4px;
|
|
49
|
+
border-bottom-right-radius: 4px;
|
|
50
|
+
border-top-color: #DADADA;
|
|
51
|
+
color: #666;
|
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
|
|
57
|
+
<body class="rails-default-error-page">
|
|
58
|
+
<!-- This file lives in public/406-unsupported-browser.html -->
|
|
59
|
+
<div class="dialog">
|
|
60
|
+
<div>
|
|
61
|
+
<h1>Your browser is not supported.</h1>
|
|
62
|
+
<p>Please upgrade your browser to continue.</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
data/spec/dummy/public/422.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<title>The change you wanted was rejected (422)</title>
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<style>
|
|
7
|
-
|
|
7
|
+
.rails-default-error-page {
|
|
8
8
|
background-color: #EFEFEF;
|
|
9
9
|
color: #2E2F30;
|
|
10
10
|
text-align: center;
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
margin: 0;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
div.dialog {
|
|
15
|
+
.rails-default-error-page div.dialog {
|
|
16
16
|
width: 95%;
|
|
17
17
|
max-width: 33em;
|
|
18
18
|
margin: 4em auto 0;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
div.dialog > div {
|
|
21
|
+
.rails-default-error-page div.dialog > div {
|
|
22
22
|
border: 1px solid #CCC;
|
|
23
23
|
border-right-color: #999;
|
|
24
24
|
border-left-color: #999;
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
h1 {
|
|
34
|
+
.rails-default-error-page h1 {
|
|
35
35
|
font-size: 100%;
|
|
36
36
|
color: #730E15;
|
|
37
37
|
line-height: 1.5em;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
div.dialog > p {
|
|
40
|
+
.rails-default-error-page div.dialog > p {
|
|
41
41
|
margin: 0 0 1em;
|
|
42
42
|
padding: 1em;
|
|
43
43
|
background-color: #F7F7F7;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
</style>
|
|
55
55
|
</head>
|
|
56
56
|
|
|
57
|
-
<body>
|
|
57
|
+
<body class="rails-default-error-page">
|
|
58
58
|
<!-- This file lives in public/422.html -->
|
|
59
59
|
<div class="dialog">
|
|
60
60
|
<div>
|
data/spec/dummy/public/500.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<title>We're sorry, but something went wrong (500)</title>
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<style>
|
|
7
|
-
|
|
7
|
+
.rails-default-error-page {
|
|
8
8
|
background-color: #EFEFEF;
|
|
9
9
|
color: #2E2F30;
|
|
10
10
|
text-align: center;
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
margin: 0;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
div.dialog {
|
|
15
|
+
.rails-default-error-page div.dialog {
|
|
16
16
|
width: 95%;
|
|
17
17
|
max-width: 33em;
|
|
18
18
|
margin: 4em auto 0;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
div.dialog > div {
|
|
21
|
+
.rails-default-error-page div.dialog > div {
|
|
22
22
|
border: 1px solid #CCC;
|
|
23
23
|
border-right-color: #999;
|
|
24
24
|
border-left-color: #999;
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
h1 {
|
|
34
|
+
.rails-default-error-page h1 {
|
|
35
35
|
font-size: 100%;
|
|
36
36
|
color: #730E15;
|
|
37
37
|
line-height: 1.5em;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
div.dialog > p {
|
|
40
|
+
.rails-default-error-page div.dialog > p {
|
|
41
41
|
margin: 0 0 1em;
|
|
42
42
|
padding: 1em;
|
|
43
43
|
background-color: #F7F7F7;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
</style>
|
|
55
55
|
</head>
|
|
56
56
|
|
|
57
|
-
<body>
|
|
57
|
+
<body class="rails-default-error-page">
|
|
58
58
|
<!-- This file lives in public/500.html -->
|
|
59
59
|
<div class="dialog">
|
|
60
60
|
<div>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::ConfigurationHelper, which renders TrustyCms::Config settings
|
|
4
|
+
# as read-only rows (show_config) and as edit fields (edit_config), branching on
|
|
5
|
+
# whether a setting is boolean, a selector, or free text.
|
|
6
|
+
describe Admin::ConfigurationHelper, type: :helper do
|
|
7
|
+
describe '#setting_for' do
|
|
8
|
+
it 'finds or initializes the config entry for a key' do
|
|
9
|
+
setting = helper.setting_for('admin.title')
|
|
10
|
+
expect(setting).to be_a(TrustyCms::Config)
|
|
11
|
+
expect(setting.key).to eq('admin.title')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'memoizes into @trusty_config' do
|
|
15
|
+
helper.setting_for('admin.title')
|
|
16
|
+
expect(helper.instance_variable_get(:@trusty_config)).to have_key('admin.title')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#definition_for' do
|
|
21
|
+
it 'returns the setting definition' do
|
|
22
|
+
expect(helper.definition_for('admin.title')).to eq(TrustyCms.config.definition_for('admin.title'))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#show_config' do
|
|
27
|
+
it 'renders a label and value span for a text setting' do
|
|
28
|
+
html = helper.show_config('admin.title')
|
|
29
|
+
expect(html).to include('<label')
|
|
30
|
+
expect(html).to include('id="admin_title"')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'renders yes/no for a boolean setting' do
|
|
34
|
+
html = helper.show_config('assets.create_image_thumbnails?')
|
|
35
|
+
expect(html).to match(/yes|no/i)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'includes a units span for a setting with units' do
|
|
39
|
+
html = helper.show_config('assets.max_asset_size')
|
|
40
|
+
expect(html).to include('class="units"')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#edit_config' do
|
|
45
|
+
it 'renders a text field for a plain text setting' do
|
|
46
|
+
html = helper.edit_config('admin.title')
|
|
47
|
+
expect(html).to include('type="text"')
|
|
48
|
+
expect(html).to include('name="trusty_config[admin.title]"')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'renders a checkbox (plus hidden field) for a boolean setting' do
|
|
52
|
+
html = helper.edit_config('assets.create_image_thumbnails?')
|
|
53
|
+
expect(html).to include('type="checkbox"')
|
|
54
|
+
expect(html).to include('type="hidden"')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'renders a select for a selector setting' do
|
|
58
|
+
html = helper.edit_config('defaults.page.status')
|
|
59
|
+
expect(html).to include('<select')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Exercises Admin::NodeHelper, which renders the admin page-tree nodes. The
|
|
4
|
+
# partial-rendering entry points are driven with `render` stubbed (the real
|
|
5
|
+
# partials need the full admin layout), while the node-presentation helpers
|
|
6
|
+
# (title, icon, expander, expansion state) are exercised directly.
|
|
7
|
+
describe Admin::NodeHelper, type: :helper do
|
|
8
|
+
let!(:home) { FactoryBot.create(:home, title: 'Home') }
|
|
9
|
+
let(:page) { FactoryBot.create(:page, title: 'Child', slug: 'child', parent: home) }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
# image() lives in ApplicationHelper (not mixed into this helper) and returns
|
|
13
|
+
# an <img> tag; stub it so the presentation helpers can be tested in isolation.
|
|
14
|
+
allow(helper).to receive(:image) { |name, *| "<img data-name='#{name}'/>".html_safe }
|
|
15
|
+
allow(helper).to receive(:cookies).and_return({})
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#prepare_page' do
|
|
19
|
+
it 'extends the page with MenuRenderer and wires up the view' do
|
|
20
|
+
result = helper.prepare_page(page)
|
|
21
|
+
expect(result).to be(page)
|
|
22
|
+
expect(result.view).to be(helper)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#homepage' do
|
|
27
|
+
it 'returns the page with no parent' do
|
|
28
|
+
expect(helper.homepage).to eq(home)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#show_all?' do
|
|
33
|
+
it 'is true only on the remove action' do
|
|
34
|
+
allow(helper.controller).to receive(:action_name).and_return('remove')
|
|
35
|
+
expect(helper.show_all?).to be(true)
|
|
36
|
+
|
|
37
|
+
allow(helper.controller).to receive(:action_name).and_return('index')
|
|
38
|
+
expect(helper.show_all?).to be(false)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#expanded_rows' do
|
|
43
|
+
it 'parses the expanded_rows cookie and always includes the homepage' do
|
|
44
|
+
allow(helper).to receive(:cookies).and_return(expanded_rows: "#{page.id},not-a-number")
|
|
45
|
+
|
|
46
|
+
rows = helper.expanded_rows
|
|
47
|
+
|
|
48
|
+
expect(rows).to include(page.id)
|
|
49
|
+
expect(rows).to include(home.id) # homepage is always expanded
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'defaults to just the homepage when no cookie is set' do
|
|
53
|
+
expect(helper.expanded_rows).to eq([home.id])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @current_node is normally set by render_node; set it directly so the
|
|
58
|
+
# presentation helpers (which read it) can be exercised in isolation.
|
|
59
|
+
def make_current(a_page)
|
|
60
|
+
helper.instance_variable_set(:@current_node, helper.prepare_page(a_page))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe 'node presentation' do
|
|
64
|
+
before { make_current(page) }
|
|
65
|
+
|
|
66
|
+
describe '#expanded' do
|
|
67
|
+
it 'is true on the remove action regardless of cookie state' do
|
|
68
|
+
allow(helper).to receive(:show_all?).and_return(true)
|
|
69
|
+
expect(helper.expanded).to be(true)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#expander' do
|
|
74
|
+
it 'is blank at the top level' do
|
|
75
|
+
expect(helper.expander(0)).to eq('')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'is blank for a childless node' do
|
|
79
|
+
make_current(page) # child has no children
|
|
80
|
+
expect(helper.expander(1)).to eq('')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'renders a toggle image for a node with children at a nested level' do
|
|
84
|
+
make_current(home) # home has the child page
|
|
85
|
+
expect(helper.expander(1)).to include('<img')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#icon' do
|
|
90
|
+
it 'renders an icon image' do
|
|
91
|
+
expect(helper.icon).to include('<img')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '#node_title' do
|
|
96
|
+
it 'wraps the escaped title in a span' do
|
|
97
|
+
expect(helper.node_title).to eq(%{<span class="title">Child</span>})
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '#page_type' do
|
|
102
|
+
it 'is blank for a plain Page' do
|
|
103
|
+
expect(helper.page_type).to eq('')
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '#spinner' do
|
|
108
|
+
it 'renders a hidden spinner image' do
|
|
109
|
+
expect(helper.spinner).to include('<img')
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe 'rendering entry points' do
|
|
115
|
+
before { allow(helper).to receive(:render).and_return('<node/>'.html_safe) }
|
|
116
|
+
|
|
117
|
+
it '#render_node accumulates rendered HTML and returns the index' do
|
|
118
|
+
helper.instance_variable_set(:@rendered_html, '') # normally done by render_nodes
|
|
119
|
+
expect(helper.render_node(page, 2)).to eq(2)
|
|
120
|
+
expect(helper.instance_variable_get(:@rendered_html)).to eq('<node/>')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it '#render_nodes resets and returns the accumulated HTML' do
|
|
124
|
+
expect(helper.render_nodes(page, 0)).to eq('<node/>')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it '#render_search_node renders the search partial' do
|
|
128
|
+
helper.render_search_node(page)
|
|
129
|
+
expect(helper.instance_variable_get(:@rendered_html)).to eq('<node/>')
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -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
|
|
@@ -99,4 +99,119 @@ describe ApplicationHelper, type: :helper do
|
|
|
99
99
|
expect(helper.body_classes).to eq(['reversed'])
|
|
100
100
|
end
|
|
101
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
|
|
102
217
|
end
|
|
@@ -44,5 +44,46 @@ describe 'String extensions' do
|
|
|
44
44
|
expect('Hello World'.slugify).to eq('hello-world')
|
|
45
45
|
expect('Hello World'.slugerize).to eq('hello-world')
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
it 'downcases by default' do
|
|
49
|
+
expect('Hello World'.parameterize).to eq('hello-world')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'preserves case when preserve_case: true' do
|
|
53
|
+
expect('Hello World'.parameterize(preserve_case: true)).to eq('Hello-World')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'uses a custom string separator' do
|
|
57
|
+
expect('Hello World'.parameterize('_')).to eq('hello_world')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'honours the separator and preserve_case together' do
|
|
61
|
+
expect('Hello World'.parameterize('_', preserve_case: true)).to eq('Hello_World')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The upgrade added the guard `separator = '-' unless separator.is_a?(String)`
|
|
65
|
+
# so that Rails internals passing a non-String (e.g. a Symbol or nil)
|
|
66
|
+
# fall back to the default rather than blowing up.
|
|
67
|
+
it 'falls back to a dash when the separator is not a String' do
|
|
68
|
+
expect('Hello World'.parameterize(:_)).to eq('hello-world')
|
|
69
|
+
expect('Hello World'.parameterize(nil)).to eq('hello-world')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `locale:` is part of the ActiveSupport-compatible signature; accepted and ignored.
|
|
73
|
+
it 'accepts a locale keyword without raising' do
|
|
74
|
+
expect('Hello World'.parameterize(locale: :en)).to eq('hello-world')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'transliterates accented characters before downcasing' do
|
|
78
|
+
expect('Café Déjà Vu'.parameterize).to eq('cafe-deja-vu')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'collapses repeated whitespace into a single separator' do
|
|
82
|
+
expect('Hello World'.parameterize).to eq('hello-world')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns an empty string for an empty string' do
|
|
86
|
+
expect(''.parameterize).to eq('')
|
|
87
|
+
end
|
|
47
88
|
end
|
|
48
89
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The app configures ActiveRecord::Encryption from ENV vars (see
|
|
2
|
+
# config/initializers/active_record_encryption.rb), which are unset in the test
|
|
3
|
+
# environment. Without keys, writing an encrypted attribute (e.g. User#otp_secret
|
|
4
|
+
# from devise-two-factor) raises a Configuration error. Configure deterministic
|
|
5
|
+
# test keys here so specs can exercise encrypted attributes.
|
|
6
|
+
ActiveRecord::Encryption.configure(
|
|
7
|
+
primary_key: 'test_primary_key_for_specs_only_0001',
|
|
8
|
+
deterministic_key: 'test_deterministic_key_for_specs_0001',
|
|
9
|
+
key_derivation_salt: 'test_key_derivation_salt_for_specs_0001'
|
|
10
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# AssetType keeps its registry in class variables (@@types, @@type_lookup,
|
|
2
|
+
# @@default_type memoized by .catchall) with no reset hook, so registrations
|
|
3
|
+
# persist for the whole suite. Rather than snapshot/restore around individual
|
|
4
|
+
# specs — which desyncs the memoized catchall when a type is removed and later
|
|
5
|
+
# re-registered as a fresh object — every spec that needs asset types registers
|
|
6
|
+
# the SAME canonical definitions, guarded by `.known?`. Whichever spec runs
|
|
7
|
+
# first wins; the rest are no-ops. Definitions here mirror spec/models/asset_spec.rb
|
|
8
|
+
# so nothing gets shadowed by a thinner definition.
|
|
9
|
+
module AssetTypeRegistry
|
|
10
|
+
def register_standard_asset_types
|
|
11
|
+
unless AssetType.known?(:image)
|
|
12
|
+
AssetType.new(:image, icon: 'image', styles: :standard,
|
|
13
|
+
extensions: %w[jpg jpeg png gif],
|
|
14
|
+
mime_types: %w[image/png image/x-png image/jpeg image/pjpeg image/jpg image/gif])
|
|
15
|
+
end
|
|
16
|
+
unless AssetType.known?(:video)
|
|
17
|
+
AssetType.new(:video, icon: 'video',
|
|
18
|
+
mime_types: %w[video/mp4 video/mpeg video/quicktime video/webm])
|
|
19
|
+
end
|
|
20
|
+
unless AssetType.known?(:document)
|
|
21
|
+
AssetType.new(:document, icon: 'document',
|
|
22
|
+
mime_types: %w[application/msword application/rtf text/plain text/html])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
RSpec.configure do |config|
|
|
28
|
+
config.include AssetTypeRegistry
|
|
29
|
+
end
|
data/trusty_cms.gemspec
CHANGED
|
@@ -42,6 +42,7 @@ a general purpose content management system--not merely a blogging engine.'
|
|
|
42
42
|
s.add_dependency 'highline', '>= 1.7.8', '< 3.2.0'
|
|
43
43
|
s.add_dependency 'image_processing'
|
|
44
44
|
s.add_dependency 'kraken-io'
|
|
45
|
+
s.add_dependency 'mini_magick', '~> 5.0'
|
|
45
46
|
s.add_dependency 'mini_racer'
|
|
46
47
|
s.add_dependency 'mutex_m'
|
|
47
48
|
s.add_dependency 'mysql2'
|
|
@@ -51,7 +52,7 @@ a general purpose content management system--not merely a blogging engine.'
|
|
|
51
52
|
s.add_dependency 'rack', '>= 2.0.1', '< 3.2.0'
|
|
52
53
|
s.add_dependency 'rack-cache', '~> 1.7'
|
|
53
54
|
s.add_dependency 'radius', '~> 0.8'
|
|
54
|
-
s.add_dependency 'rails', '~>
|
|
55
|
+
s.add_dependency 'rails', '~> 8.0.0'
|
|
55
56
|
s.add_dependency 'rake', '< 14.0'
|
|
56
57
|
s.add_dependency 'ransack', '~> 4.2.1'
|
|
57
58
|
s.add_dependency 'rdoc', '>= 5.1', '< 9.0'
|