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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -3
  3. data/Gemfile.lock +58 -46
  4. data/README.md +2 -0
  5. data/app/controllers/admin/resource_controller.rb +1 -1
  6. data/app/controllers/application_controller.rb +15 -0
  7. data/app/models/asset.rb +29 -5
  8. data/app/models/standard_tags.rb +13 -7
  9. data/app/models/user.rb +13 -0
  10. data/lib/trusty_cms/site_scope_auth_reporter.rb +108 -0
  11. data/lib/trusty_cms/version.rb +1 -1
  12. data/package.json +1 -1
  13. data/spec/controllers/admin/snippets_controller_spec.rb +95 -0
  14. data/spec/controllers/site_controller_spec.rb +63 -0
  15. data/spec/factories/site.rb +8 -0
  16. data/spec/helpers/admin/references_helper_spec.rb +54 -0
  17. data/spec/helpers/admin/url_helper_spec.rb +71 -0
  18. data/spec/helpers/application_helper_spec.rb +102 -0
  19. data/spec/helpers/scoped_helper_spec.rb +60 -0
  20. data/spec/lib/active_record_extensions_spec.rb +65 -0
  21. data/spec/lib/login_system_spec.rb +61 -0
  22. data/spec/lib/ostruct_spec.rb +33 -0
  23. data/spec/lib/string_extensions_spec.rb +48 -0
  24. data/spec/lib/symbol_extensions_spec.rb +14 -0
  25. data/spec/lib/trusty_cms/config_cache_spec.rb +73 -0
  26. data/spec/models/admins_site_spec.rb +19 -0
  27. data/spec/models/asset_spec.rb +224 -0
  28. data/spec/models/asset_type_spec.rb +137 -0
  29. data/spec/models/file_not_found_page_spec.rb +33 -0
  30. data/spec/models/haml_filter_spec.rb +30 -0
  31. data/spec/models/menu_renderer_spec.rb +63 -0
  32. data/spec/models/page_attachment_spec.rb +29 -0
  33. data/spec/models/page_context_spec.rb +57 -0
  34. data/spec/models/page_field_spec.rb +15 -0
  35. data/spec/models/page_part_spec.rb +33 -0
  36. data/spec/models/page_spec.rb +184 -0
  37. data/spec/models/rails_page_spec.rb +32 -0
  38. data/spec/models/site_spec.rb +93 -0
  39. data/spec/models/snippet_finder_spec.rb +27 -0
  40. data/spec/models/snippet_tags_spec.rb +45 -0
  41. data/spec/models/standard_tags/children_spec.rb +159 -0
  42. data/spec/models/standard_tags/content_spec.rb +168 -0
  43. data/spec/models/standard_tags/meta_spec.rb +86 -0
  44. data/spec/models/standard_tags_spec.rb +32 -0
  45. data/spec/models/status_spec.rb +63 -0
  46. data/spec/models/text_filter_spec.rb +47 -0
  47. data/spec/models/trusty_cms/config_spec.rb +69 -0
  48. data/spec/models/trusty_cms/page_response_cache_director_spec.rb +72 -0
  49. data/spec/models/user_action_observer_spec.rb +39 -0
  50. data/spec/models/user_serialize_telemetry_spec.rb +124 -0
  51. data/spec/rails_helper.rb +13 -6
  52. data/spec/requests/admin/snippets_spec.rb +81 -0
  53. data/spec/spec_helper.rb +17 -3
  54. data/trusty_cms.gemspec +3 -3
  55. data/yarn.lock +273 -259
  56. 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 &amp; Jerry &lt;b&gt;" />')
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 &amp; 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 &amp; 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