trusty-cms 7.1.2 → 7.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +4 -3
- data/Gemfile.lock +58 -46
- 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/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/snippets_controller_spec.rb +95 -0
- data/spec/controllers/site_controller_spec.rb +63 -0
- data/spec/factories/site.rb +8 -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 +102 -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 +48 -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/trusty_cms.gemspec +3 -3
- data/yarn.lock +273 -259
- metadata +91 -8
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'StandardTags: <r:content>' do
|
|
4
|
+
# Render radius markup in the context of the given page.
|
|
5
|
+
def render(page, input)
|
|
6
|
+
page.send(:parse, input)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:page) { FactoryBot.create(:page, title: 'Home') }
|
|
10
|
+
|
|
11
|
+
# 1. Happy path: renders the default `body` part.
|
|
12
|
+
it 'renders the body part by default' do
|
|
13
|
+
page.parts.create(name: 'body', content: 'Hello world')
|
|
14
|
+
expect(render(page, '<r:content />')).to eq('Hello world')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 2. Named part via the `part` attribute.
|
|
18
|
+
it 'renders a named part when given the part attribute' do
|
|
19
|
+
page.parts.create(name: 'sidebar', content: 'Sidebar text')
|
|
20
|
+
expect(render(page, '<r:content part="sidebar" />')).to eq('Sidebar text')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 3. Missing part -> `part` is nil -> renders empty (line 500 leaves result nil).
|
|
24
|
+
it 'renders nothing when the requested part does not exist' do
|
|
25
|
+
expect(render(page, '<r:content part="nonexistent" />')).to eq('')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 7. Nested radius tags inside the part are parsed by render_snippet.
|
|
29
|
+
it 'parses radius tags contained in the part' do
|
|
30
|
+
page.parts.create(name: 'body', content: 'Title is <r:title />')
|
|
31
|
+
expect(render(page, '<r:content />')).to eq('Title is Home')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'inherit' do
|
|
35
|
+
let(:parent) { FactoryBot.create(:page, title: 'Parent') }
|
|
36
|
+
let(:child) { FactoryBot.create(:page, title: 'Child', parent: parent) }
|
|
37
|
+
|
|
38
|
+
before { parent.parts.create(name: 'body', content: 'Parent body') }
|
|
39
|
+
|
|
40
|
+
# 4. inherit="true" walks up to the parent's part.
|
|
41
|
+
it 'renders an inherited part from the parent when inherit is true' do
|
|
42
|
+
expect(render(child, '<r:content inherit="true" />')).to eq('Parent body')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 5. Default (inherit=false) does not walk up.
|
|
46
|
+
it 'renders nothing when the part is missing and inherit is false' do
|
|
47
|
+
expect(render(child, '<r:content />')).to eq('')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 6. Recursion guard raises TagError (errors are raised in the test env).
|
|
52
|
+
it 'raises a TagError on recursive rendering of the same part' do
|
|
53
|
+
page.parts.create(name: 'body', content: 'loop <r:content />')
|
|
54
|
+
expect { render(page, '<r:content />') }
|
|
55
|
+
.to raise_error(StandardTags::TagError, /Recursion error/)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 8. A non-boolean value for a boolean attribute raises TagError.
|
|
59
|
+
it 'raises a TagError when inherit is not a boolean' do
|
|
60
|
+
page.parts.create(name: 'body', content: 'Hello')
|
|
61
|
+
expect { render(page, '<r:content inherit="maybe" />') }
|
|
62
|
+
.to raise_error(StandardTags::TagError)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe 'StandardTags: <r:if_content>' do
|
|
67
|
+
def render(page, input)
|
|
68
|
+
page.send(:parse, input)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
let(:page) { FactoryBot.create(:page, title: 'Home') }
|
|
72
|
+
|
|
73
|
+
it 'expands when the default body part exists' do
|
|
74
|
+
page.parts.create(name: 'body', content: 'x')
|
|
75
|
+
expect(render(page, '<r:if_content>shown</r:if_content>')).to eq('shown')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'does not expand when the part is missing' do
|
|
79
|
+
expect(render(page, '<r:if_content>shown</r:if_content>')).to eq('')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe 'multiple parts' do
|
|
83
|
+
before { page.parts.create(name: 'body', content: 'x') }
|
|
84
|
+
|
|
85
|
+
it 'find="all" (default) does not expand when one listed part is missing' do
|
|
86
|
+
expect(render(page, '<r:if_content part="body, sidebar">shown</r:if_content>')).to eq('')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'find="all" expands when every listed part exists' do
|
|
90
|
+
page.parts.create(name: 'sidebar', content: 'y')
|
|
91
|
+
expect(render(page, '<r:if_content part="body, sidebar">shown</r:if_content>')).to eq('shown')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'find="any" expands when at least one listed part exists' do
|
|
95
|
+
expect(render(page, '<r:if_content part="body, sidebar" find="any">shown</r:if_content>')).to eq('shown')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe 'inherit' do
|
|
100
|
+
let(:parent) { FactoryBot.create(:page, title: 'Parent') }
|
|
101
|
+
let(:child) { FactoryBot.create(:page, title: 'Child', parent: parent) }
|
|
102
|
+
|
|
103
|
+
before { parent.parts.create(name: 'body', content: 'Parent body') }
|
|
104
|
+
|
|
105
|
+
it 'expands when an ancestor has the part and inherit is true' do
|
|
106
|
+
expect(render(child, '<r:if_content inherit="true">shown</r:if_content>')).to eq('shown')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'does not expand for an inherited part when inherit is false' do
|
|
110
|
+
expect(render(child, '<r:if_content>shown</r:if_content>')).to eq('')
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'raises a TagError for an invalid find value' do
|
|
115
|
+
page.parts.create(name: 'body', content: 'x')
|
|
116
|
+
expect { render(page, '<r:if_content find="bogus">shown</r:if_content>') }
|
|
117
|
+
.to raise_error(StandardTags::TagError)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe 'StandardTags: <r:unless_content>' do
|
|
122
|
+
def render(page, input)
|
|
123
|
+
page.send(:parse, input)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
let(:page) { FactoryBot.create(:page, title: 'Home') }
|
|
127
|
+
|
|
128
|
+
it 'expands when the default body part is missing' do
|
|
129
|
+
expect(render(page, '<r:unless_content>shown</r:unless_content>')).to eq('shown')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'does not expand when the part exists' do
|
|
133
|
+
page.parts.create(name: 'body', content: 'x')
|
|
134
|
+
expect(render(page, '<r:unless_content>shown</r:unless_content>')).to eq('')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe 'multiple parts' do
|
|
138
|
+
before { page.parts.create(name: 'body', content: 'x') }
|
|
139
|
+
|
|
140
|
+
it 'find="all" (default) expands when at least one listed part is missing' do
|
|
141
|
+
expect(render(page, '<r:unless_content part="body, sidebar">shown</r:unless_content>')).to eq('shown')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'find="any" does not expand when any listed part is found' do
|
|
145
|
+
expect(render(page, '<r:unless_content part="body, sidebar" find="any">shown</r:unless_content>')).to eq('')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'find="any" expands when none of the listed parts exist' do
|
|
149
|
+
expect(render(page, '<r:unless_content part="header, sidebar" find="any">shown</r:unless_content>')).to eq('shown')
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe 'inherit' do
|
|
154
|
+
let(:parent) { FactoryBot.create(:page, title: 'Parent') }
|
|
155
|
+
let(:child) { FactoryBot.create(:page, title: 'Child', parent: parent) }
|
|
156
|
+
|
|
157
|
+
before { parent.parts.create(name: 'body', content: 'Parent body') }
|
|
158
|
+
|
|
159
|
+
it 'does not expand when an ancestor has the part and inherit is true' do
|
|
160
|
+
expect(render(child, '<r:unless_content inherit="true">shown</r:unless_content>')).to eq('')
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'raises a TagError for an invalid find value' do
|
|
165
|
+
expect { render(page, '<r:unless_content find="bogus">shown</r:unless_content>') }
|
|
166
|
+
.to raise_error(StandardTags::TagError)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'StandardTags: <r:meta>' do
|
|
4
|
+
def render(page, input)
|
|
5
|
+
page.send(:parse, input)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:page) { FactoryBot.create(:page, title: 'Home') }
|
|
9
|
+
|
|
10
|
+
describe '<r:meta:description>' do
|
|
11
|
+
it 'wraps the description field in a meta element by default' do
|
|
12
|
+
page.fields.create(name: 'description', content: 'A page')
|
|
13
|
+
expect(render(page, '<r:meta:description />'))
|
|
14
|
+
.to eq('<meta name="description" content="A page" />')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'escapes HTML in the description content' do
|
|
18
|
+
page.fields.create(name: 'description', content: 'Tom & Jerry <b>')
|
|
19
|
+
expect(render(page, '<r:meta:description />'))
|
|
20
|
+
.to eq('<meta name="description" content="Tom & Jerry <b>" />')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns the raw (escaped) content without a wrapper when tag="false"' do
|
|
24
|
+
page.fields.create(name: 'description', content: 'Tom & Jerry')
|
|
25
|
+
expect(render(page, '<r:meta:description tag="false" />')).to eq('Tom & Jerry')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'renders an empty content element when there is no description field' do
|
|
29
|
+
expect(render(page, '<r:meta:description />'))
|
|
30
|
+
.to eq('<meta name="description" content="" />')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '<r:meta:keywords>' do
|
|
35
|
+
it 'wraps the keywords field in a meta element by default' do
|
|
36
|
+
page.fields.create(name: 'keywords', content: 'ruby, cms')
|
|
37
|
+
expect(render(page, '<r:meta:keywords />'))
|
|
38
|
+
.to eq('<meta name="keywords" content="ruby, cms" />')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'returns the raw (escaped) content without a wrapper when tag="false"' do
|
|
42
|
+
page.fields.create(name: 'keywords', content: 'a & b')
|
|
43
|
+
expect(render(page, '<r:meta:keywords tag="false" />')).to eq('a & b')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'renders an empty content element when there is no keywords field' do
|
|
47
|
+
expect(render(page, '<r:meta:keywords />'))
|
|
48
|
+
.to eq('<meta name="keywords" content="" />')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '<r:meta>' do
|
|
53
|
+
it 'concatenates the description and keywords elements when used as a single tag' do
|
|
54
|
+
page.fields.create(name: 'description', content: 'A page')
|
|
55
|
+
page.fields.create(name: 'keywords', content: 'ruby')
|
|
56
|
+
expect(render(page, '<r:meta />'))
|
|
57
|
+
.to eq('<meta name="description" content="A page" /><meta name="keywords" content="ruby" />')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'expands its contents when used as a double tag' do
|
|
61
|
+
expect(render(page, '<r:meta>inner <r:title /></r:meta>')).to eq('inner Home')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe 'StandardTags: <r:site>' do
|
|
67
|
+
def render(page, input)
|
|
68
|
+
page.send(:parse, input)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
let(:page) { FactoryBot.create(:page, title: 'Home') }
|
|
72
|
+
|
|
73
|
+
it 'expands the contents of the site container tag' do
|
|
74
|
+
expect(render(page, '<r:site>content</r:site>')).to eq('content')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'renders the configured site title' do
|
|
78
|
+
TrustyCms::Config['site.title'] = 'My Site'
|
|
79
|
+
expect(render(page, '<r:site:title />')).to eq('My Site')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'renders the configured site host' do
|
|
83
|
+
TrustyCms::Config['site.host'] = 'example.com'
|
|
84
|
+
expect(render(page, '<r:site:host />')).to eq('example.com')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Standard Tags' do
|
|
4
|
+
# Helper: render a radius string in the context of the given page.
|
|
5
|
+
def render(page, input)
|
|
6
|
+
page.send(:parse, input)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:home) do
|
|
10
|
+
FactoryBot.create(:home, title: 'Home', breadcrumb: 'Home')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '<r:page>' do
|
|
14
|
+
it 'sets the local page to the global page' do
|
|
15
|
+
expect(render(home, '<r:page><r:title /></r:page>')).to eq('Home')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe 'attribute tags' do
|
|
20
|
+
it 'renders <r:title />' do
|
|
21
|
+
expect(render(home, '<r:title />')).to eq('Home')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'renders <r:breadcrumb />' do
|
|
25
|
+
expect(render(home, '<r:breadcrumb />')).to eq('Home')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'renders <r:slug />' do
|
|
29
|
+
expect(render(home, '<r:slug />')).to eq('/')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Status do
|
|
4
|
+
describe '#initialize' do
|
|
5
|
+
it 'assigns id and name from options' do
|
|
6
|
+
status = Status.new(id: 1, name: 'Draft')
|
|
7
|
+
expect(status.id).to eq(1)
|
|
8
|
+
expect(status.name).to eq('Draft')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'accepts string keys' do
|
|
12
|
+
status = Status.new('id' => 1, 'name' => 'Draft')
|
|
13
|
+
expect(status.id).to eq(1)
|
|
14
|
+
expect(status.name).to eq('Draft')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#symbol' do
|
|
19
|
+
it 'returns the downcased name as a symbol' do
|
|
20
|
+
expect(Status.new(name: 'Published').symbol).to eq(:published)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.[]' do
|
|
25
|
+
it 'finds a status by its symbol' do
|
|
26
|
+
expect(Status[:published].name).to eq('Published')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'is case-insensitive' do
|
|
30
|
+
expect(Status['Published']).to eq(Status[:published])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'returns nil for an unknown symbol' do
|
|
34
|
+
expect(Status[:nonexistent]).to be_nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.find' do
|
|
39
|
+
it 'finds a status by its id' do
|
|
40
|
+
expect(Status.find(100).name).to eq('Published')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'matches ids regardless of type' do
|
|
44
|
+
expect(Status.find('100')).to eq(Status.find(100))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns nil for an unknown id' do
|
|
48
|
+
expect(Status.find(999)).to be_nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '.selectable' do
|
|
53
|
+
it 'returns all defined statuses' do
|
|
54
|
+
expect(Status.selectable.map(&:name)).to eq(%w[Draft Reviewed Scheduled Published Hidden])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '.selectable_values' do
|
|
59
|
+
it 'returns the names of all selectable statuses' do
|
|
60
|
+
expect(Status.selectable_values).to eq(%w[Draft Reviewed Scheduled Published Hidden])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Defined at load time so TextFilter.inherited fires and registers a descendant.
|
|
4
|
+
class SpecSampleFilter < TextFilter
|
|
5
|
+
def filter(text)
|
|
6
|
+
"[#{text}]"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe TextFilter do
|
|
11
|
+
describe '#filter' do
|
|
12
|
+
it 'returns the text unchanged for the base filter' do
|
|
13
|
+
expect(TextFilter.new.filter('hello')).to eq('hello')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '.filter' do
|
|
18
|
+
it 'delegates to the singleton instance' do
|
|
19
|
+
expect(TextFilter.filter('hello')).to eq('hello')
|
|
20
|
+
expect(SpecSampleFilter.filter('hello')).to eq('[hello]')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.inherited' do
|
|
25
|
+
it 'derives a filter_name from the subclass name' do
|
|
26
|
+
expect(SpecSampleFilter.filter_name).to eq('Spec Sample')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '.descendants_names' do
|
|
31
|
+
it 'includes the names of registered descendants, sorted' do
|
|
32
|
+
names = TextFilter.descendants_names
|
|
33
|
+
expect(names).to include('Spec Sample')
|
|
34
|
+
expect(names).to eq(names.sort)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.find_descendant' do
|
|
39
|
+
it 'finds a descendant by its filter_name' do
|
|
40
|
+
expect(TextFilter.find_descendant('Spec Sample')).to eq(SpecSampleFilter)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'returns nil when no descendant matches' do
|
|
44
|
+
expect(TextFilter.find_descendant('No Such Filter')).to be_nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe TrustyCms::Config do
|
|
4
|
+
# The custom #value= setter calls save!, and the config table is exempt from
|
|
5
|
+
# DatabaseCleaner truncation, so a saved row would leak across examples and
|
|
6
|
+
# collide on the unique key index. Build unsaved instances and assign the raw
|
|
7
|
+
# attribute with self[:value]= (write_attribute) to bypass the setter.
|
|
8
|
+
def config(key, value = nil)
|
|
9
|
+
TrustyCms::Config.new(key: key).tap do |c|
|
|
10
|
+
c[:value] = value unless value.nil?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#boolean?' do
|
|
15
|
+
it 'is true when the key ends with a question mark' do
|
|
16
|
+
expect(config('feature.enabled?').boolean?).to be(true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'is false for an ordinary key with no definition' do
|
|
20
|
+
expect(config('feature.name').boolean?).to be(false)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#checked?' do
|
|
25
|
+
it 'is true for a boolean setting whose value is "true"' do
|
|
26
|
+
expect(config('x?', 'true').checked?).to be(true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'is false for a boolean setting whose value is not "true"' do
|
|
30
|
+
expect(config('x?', 'false').checked?).to be(false)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#value' do
|
|
35
|
+
it 'returns a boolean for a boolean setting' do
|
|
36
|
+
expect(config('x?', 'true').value).to be(true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns the raw string for a non-boolean setting' do
|
|
40
|
+
expect(config('plain', 'hello').value).to eq('hello')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#selector?' do
|
|
45
|
+
it 'is false when the definition does not restrict the value' do
|
|
46
|
+
expect(config('plain').selector?).to be(false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#definition' do
|
|
51
|
+
it 'returns a Definition even when none was declared' do
|
|
52
|
+
expect(config('undeclared').definition).to be_a(TrustyCms::Config::Definition)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '.site_settings' do
|
|
57
|
+
it 'lists the site-level settings' do
|
|
58
|
+
expect(TrustyCms::Config.site_settings).to eq(%w[site.title site.host local.timezone])
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '.default_settings' do
|
|
63
|
+
it 'lists the default settings' do
|
|
64
|
+
expect(TrustyCms::Config.default_settings).to eq(
|
|
65
|
+
%w[defaults.locale defaults.page.filter defaults.page.parts defaults.page.fields defaults.page.status]
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe TrustyCms::PageResponseCacheDirector do
|
|
4
|
+
let(:listener) { double('listener') }
|
|
5
|
+
|
|
6
|
+
subject(:director) { described_class.new(page, listener) }
|
|
7
|
+
|
|
8
|
+
describe '.cache_timeout' do
|
|
9
|
+
around do |example|
|
|
10
|
+
original = described_class.cache_timeout
|
|
11
|
+
example.run
|
|
12
|
+
described_class.cache_timeout = original
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'defaults to five minutes' do
|
|
16
|
+
described_class.instance_variable_set(:@cache_timeout, nil)
|
|
17
|
+
expect(described_class.cache_timeout).to eq(5.minutes)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'can be overridden' do
|
|
21
|
+
described_class.cache_timeout = 10.minutes
|
|
22
|
+
expect(described_class.cache_timeout).to eq(10.minutes)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#set_cache_control' do
|
|
27
|
+
context 'when the request and page are both cacheable' do
|
|
28
|
+
before { allow(listener).to receive(:cacheable_request?).and_return(true) }
|
|
29
|
+
|
|
30
|
+
context 'and the page defines its own cache_timeout' do
|
|
31
|
+
let(:page) { double('page', cache?: true, cache_timeout: 2.minutes) }
|
|
32
|
+
|
|
33
|
+
it 'sets a public expiry using the page timeout' do
|
|
34
|
+
expect(listener).to receive(:set_expiry).with(2.minutes, public: true, private: false)
|
|
35
|
+
director.set_cache_control
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'and the page has no cache_timeout' do
|
|
40
|
+
let(:page) { double('page', cache?: true) }
|
|
41
|
+
|
|
42
|
+
it 'sets a public expiry using the class timeout' do
|
|
43
|
+
allow(described_class).to receive(:cache_timeout).and_return(5.minutes)
|
|
44
|
+
expect(listener).to receive(:set_expiry).with(5.minutes, public: true, private: false)
|
|
45
|
+
director.set_cache_control
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context 'when the page is not cacheable' do
|
|
51
|
+
let(:page) { double('page', cache?: false) }
|
|
52
|
+
before { allow(listener).to receive(:cacheable_request?).and_return(true) }
|
|
53
|
+
|
|
54
|
+
it 'sends a no-cache, private response and clears the etag' do
|
|
55
|
+
expect(listener).to receive(:set_expiry).with(nil, private: true, 'no-cache' => true)
|
|
56
|
+
expect(listener).to receive(:set_etag).with('')
|
|
57
|
+
director.set_cache_control
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'when the request is not cacheable' do
|
|
62
|
+
let(:page) { double('page', cache?: true) }
|
|
63
|
+
before { allow(listener).to receive(:cacheable_request?).and_return(false) }
|
|
64
|
+
|
|
65
|
+
it 'sends a non-cacheable response' do
|
|
66
|
+
expect(listener).to receive(:set_expiry).with(nil, private: true, 'no-cache' => true)
|
|
67
|
+
expect(listener).to receive(:set_etag).with('')
|
|
68
|
+
director.set_cache_control
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe UserActionObserver do
|
|
4
|
+
let(:observer) { UserActionObserver.instance }
|
|
5
|
+
let(:user) { User.new }
|
|
6
|
+
|
|
7
|
+
# Restore the original thread-local rather than clobbering it with nil, so
|
|
8
|
+
# the spec stays isolated and order-independent.
|
|
9
|
+
around do |example|
|
|
10
|
+
original = Thread.current[:current_user]
|
|
11
|
+
example.run
|
|
12
|
+
ensure
|
|
13
|
+
Thread.current[:current_user] = original
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'current_user' do
|
|
17
|
+
it 'stores the current user on the class in thread-local storage' do
|
|
18
|
+
UserActionObserver.current_user = user
|
|
19
|
+
expect(UserActionObserver.current_user).to eq(user)
|
|
20
|
+
expect(Thread.current[:current_user]).to eq(user)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'delegates the instance writer and reader to the class' do
|
|
24
|
+
observer.current_user = user
|
|
25
|
+
expect(UserActionObserver.current_user).to eq(user)
|
|
26
|
+
expect(observer.current_user).to eq(user)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'callbacks' do
|
|
31
|
+
it 'does not raise on before_create' do
|
|
32
|
+
expect { observer.before_create(user) }.not_to raise_error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'does not raise on before_update' do
|
|
36
|
+
expect { observer.before_update(user) }.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|