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,89 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'string_extensions/string_extensions'
|
|
3
|
+
|
|
4
|
+
# Core-class monkeypatch: pin the behaviour so a Ruby/Rails upgrade that shifts
|
|
5
|
+
# String#underscore/#humanize or stringex surfaces as a failure here.
|
|
6
|
+
describe 'String extensions' do
|
|
7
|
+
describe '#symbolize' do
|
|
8
|
+
it 'collapses non-alphanumerics and underscores into a symbol' do
|
|
9
|
+
expect('Foo Bar!'.symbolize).to eq(:foo_bar)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'strips leading and trailing separators' do
|
|
13
|
+
expect(' Hello--World '.symbolize).to eq(:hello_world)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#titlecase' do
|
|
18
|
+
it 'upcases the first letter of each word' do
|
|
19
|
+
expect('hello world'.titlecase).to eq('Hello World')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'leaves already-capitalised words alone' do
|
|
23
|
+
expect('Hello World'.titlecase).to eq('Hello World')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#to_name' do
|
|
28
|
+
it 'humanises and title-cases a class-style name' do
|
|
29
|
+
expect('FooBar'.to_name).to eq('Foo Bar')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'strips a trailing suffix when given one' do
|
|
33
|
+
expect('FooFilter'.to_name('Filter')).to eq('Foo')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#parameterize and its aliases' do
|
|
38
|
+
it 'produces a url-style slug' do
|
|
39
|
+
expect('Hello World'.parameterize).to eq('hello-world')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'aliases to_slug, slugify and slugerize to parameterize' do
|
|
43
|
+
expect('Hello World'.to_slug).to eq('hello-world')
|
|
44
|
+
expect('Hello World'.slugify).to eq('hello-world')
|
|
45
|
+
expect('Hello World'.slugerize).to eq('hello-world')
|
|
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
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
# NOTE: symbol_extensions is not required anywhere in the app itself, so we
|
|
3
|
+
# require it explicitly here. Kept as an upgrade tripwire since it depends on
|
|
4
|
+
# String#symbolize (see string_extensions).
|
|
5
|
+
require 'string_extensions/string_extensions'
|
|
6
|
+
require 'symbol_extensions/symbol_extensions'
|
|
7
|
+
|
|
8
|
+
describe 'Symbol extensions' do
|
|
9
|
+
describe '#symbolize' do
|
|
10
|
+
it 'normalises the symbol via String#symbolize' do
|
|
11
|
+
expect(:'Foo Bar'.symbolize).to eq(:foo_bar)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# The cache layer of TrustyCms::Config leans on Rails.cache and File.mtime,
|
|
4
|
+
# both of which can shift across Rails upgrades. Pin the round-trip.
|
|
5
|
+
describe TrustyCms::Config, 'caching' do
|
|
6
|
+
# Seed a known key so the read/round-trip assertions can't pass vacuously on
|
|
7
|
+
# an empty config table. The config table is exempt from truncation, so this
|
|
8
|
+
# probe row is removed explicitly after each example.
|
|
9
|
+
let(:probe_key) { 'spec.cache_probe' }
|
|
10
|
+
let(:probe_value) { 'probe-value' }
|
|
11
|
+
|
|
12
|
+
before { TrustyCms::Config[probe_key] = probe_value }
|
|
13
|
+
|
|
14
|
+
after do
|
|
15
|
+
TrustyCms::Config.where(key: probe_key).delete_all
|
|
16
|
+
TrustyCms::Config.initialize_cache # leave the cache good for whatever runs next
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '.ensure_cache_file' do
|
|
20
|
+
it 'creates the cache file' do
|
|
21
|
+
File.delete(TrustyCms::Config.cache_file) if File.exist?(TrustyCms::Config.cache_file)
|
|
22
|
+
expect(TrustyCms::Config.cache_file_exists?).to be(false)
|
|
23
|
+
|
|
24
|
+
TrustyCms::Config.ensure_cache_file
|
|
25
|
+
expect(TrustyCms::Config.cache_file_exists?).to be(true)
|
|
26
|
+
expect(File.file?(TrustyCms::Config.cache_file)).to be(true)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '.initialize_cache' do
|
|
31
|
+
it 'writes the full config hash into Rails.cache' do
|
|
32
|
+
TrustyCms::Config.initialize_cache
|
|
33
|
+
expect(Rails.cache.read('TrustyCms::Config')).to eq(TrustyCms::Config.to_hash)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'leaves the cache non-stale' do
|
|
37
|
+
TrustyCms::Config.initialize_cache
|
|
38
|
+
expect(TrustyCms::Config.stale_cache?).to be(false)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '.stale_cache?' do
|
|
43
|
+
it 'is true when the stored mtime no longer matches the cache file' do
|
|
44
|
+
TrustyCms::Config.initialize_cache
|
|
45
|
+
Rails.cache.write('TrustyCms.cache_mtime', Time.at(0))
|
|
46
|
+
expect(TrustyCms::Config.stale_cache?).to be(true)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '.[]' do
|
|
51
|
+
it 'reads a value through the cache' do
|
|
52
|
+
TrustyCms::Config.initialize_cache
|
|
53
|
+
expect(TrustyCms::Config[probe_key]).to eq(probe_value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'rebuilds a stale cache before returning a value' do
|
|
57
|
+
TrustyCms::Config.initialize_cache
|
|
58
|
+
Rails.cache.write('TrustyCms.cache_mtime', Time.at(0)) # force staleness
|
|
59
|
+
|
|
60
|
+
expect(TrustyCms::Config[probe_key]).to eq(probe_value)
|
|
61
|
+
expect(TrustyCms::Config.stale_cache?).to be(false)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '.to_hash' do
|
|
66
|
+
it 'returns a hash keyed by config key, including seeded entries' do
|
|
67
|
+
hash = TrustyCms::Config.to_hash
|
|
68
|
+
expect(hash).to be_a(Hash)
|
|
69
|
+
expect(hash).to include(probe_key => probe_value)
|
|
70
|
+
expect(hash.keys).to all(be_a(String))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe AdminsSite do
|
|
4
|
+
it 'uses the admins_sites table' do
|
|
5
|
+
expect(AdminsSite.table_name).to eq('admins_sites')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe 'associations' do
|
|
9
|
+
it 'belongs to an admin that is a User' do
|
|
10
|
+
association = AdminsSite.reflect_on_association(:admin)
|
|
11
|
+
expect(association.macro).to eq(:belongs_to)
|
|
12
|
+
expect(association.options[:class_name]).to eq('User')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'belongs to a site' do
|
|
16
|
+
expect(AdminsSite.reflect_on_association(:site).macro).to eq(:belongs_to)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/spec/models/asset_spec.rb
CHANGED
|
@@ -259,4 +259,228 @@ RSpec.describe Asset, type: :model do
|
|
|
259
259
|
expect(asset.public_url('normal')).to eq('https://s3.amazonaws.com/bucket/randomlegacykey')
|
|
260
260
|
end
|
|
261
261
|
end
|
|
262
|
+
|
|
263
|
+
# Shared helpers for the blocks below.
|
|
264
|
+
def register_image_and_other_types
|
|
265
|
+
AssetType.new(:image, styles: :standard, extensions: %w[png], mime_types: %w[image/png]) unless AssetType.known?(:image)
|
|
266
|
+
AssetType.new(:other, icon: 'unknown') unless AssetType.known?(:other)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def png_bytes
|
|
270
|
+
Base64.decode64('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==')
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def attach_png(filename: 'pixel.png', **attrs)
|
|
274
|
+
io = StringIO.new(png_bytes)
|
|
275
|
+
io.set_encoding(Encoding::BINARY)
|
|
276
|
+
described_class.new(attrs).tap do |asset|
|
|
277
|
+
asset.asset.attach(io: io, filename: filename, content_type: 'image/png')
|
|
278
|
+
asset.save!
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
describe 'unattached accessors' do
|
|
283
|
+
before { register_image_and_other_types }
|
|
284
|
+
|
|
285
|
+
it 'falls back to the stored columns when nothing is attached' do
|
|
286
|
+
asset = described_class.new(asset_file_name: 'legacy.PNG', asset_content_type: 'image/png', asset_file_size: 42)
|
|
287
|
+
|
|
288
|
+
expect(asset.filename).to eq('legacy.PNG')
|
|
289
|
+
expect(asset.content_type).to eq('image/png')
|
|
290
|
+
expect(asset.byte_size).to eq(42)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'derives basename and extension from the stored filename' do
|
|
294
|
+
asset = described_class.new(asset_file_name: 'legacy.PNG')
|
|
295
|
+
|
|
296
|
+
expect(asset.basename).to eq('legacy')
|
|
297
|
+
expect(asset.original_extension).to eq('png')
|
|
298
|
+
expect(asset.extension('original')).to eq('png')
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'returns the original extension for a style with no defined format' do
|
|
302
|
+
asset = described_class.new(asset_file_name: 'legacy.png')
|
|
303
|
+
|
|
304
|
+
expect(asset.extension('no_such_style')).to eq('png')
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
describe 'geometry' do
|
|
309
|
+
before { register_image_and_other_types }
|
|
310
|
+
|
|
311
|
+
it 'reports width, height and aspect from the original dimensions' do
|
|
312
|
+
asset = described_class.new(original_width: 100, original_height: 50)
|
|
313
|
+
|
|
314
|
+
expect(asset.width).to eq(100)
|
|
315
|
+
expect(asset.height).to eq(50)
|
|
316
|
+
expect(asset.aspect).to eq(2.0)
|
|
317
|
+
expect(asset.dimensions_known?).to be(true)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'reports a horizontal orientation for a wide asset' do
|
|
321
|
+
asset = described_class.new(original_width: 100, original_height: 50)
|
|
322
|
+
|
|
323
|
+
expect(asset.orientation).to eq('horizontal')
|
|
324
|
+
expect(asset.horizontal?).to be(true)
|
|
325
|
+
expect(asset.vertical?).to be(false)
|
|
326
|
+
expect(asset.square?).to be(false)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it 'reports a vertical orientation for a tall asset' do
|
|
330
|
+
asset = described_class.new(original_width: 50, original_height: 100)
|
|
331
|
+
|
|
332
|
+
expect(asset.orientation).to eq('vertical')
|
|
333
|
+
expect(asset.vertical?).to be(true)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
it 'reports a square orientation for an equal-sided asset' do
|
|
337
|
+
asset = described_class.new(original_width: 100, original_height: 100)
|
|
338
|
+
|
|
339
|
+
expect(asset.orientation).to eq('square')
|
|
340
|
+
expect(asset.square?).to be(true)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'is not dimensions_known? without stored dimensions' do
|
|
344
|
+
expect(described_class.new.dimensions_known?).to be(false)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it 'raises a StyleError for a style that is not defined' do
|
|
348
|
+
asset = described_class.new(original_width: 100, original_height: 50)
|
|
349
|
+
|
|
350
|
+
expect { asset.geometry('no_such_style') }.to raise_error(TrustyCms::StyleError)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
describe '#active_storage_transformations' do
|
|
355
|
+
subject(:asset) { described_class.new }
|
|
356
|
+
|
|
357
|
+
it 'resizes to fill for cropping modifiers' do
|
|
358
|
+
expect(asset.send(:active_storage_transformations, '100x80#')).to eq(resize_to_fill: [100, 80])
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it 'resizes to limit for the shrink modifier' do
|
|
362
|
+
expect(asset.send(:active_storage_transformations, '100x80>')).to eq(resize_to_limit: [100, 80])
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it 'resizes to limit when there is no modifier' do
|
|
366
|
+
expect(asset.send(:active_storage_transformations, '100x80')).to eq(resize_to_limit: [100, 80])
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it 'returns no transformations for a blank geometry' do
|
|
370
|
+
expect(asset.send(:active_storage_transformations, '')).to eq({})
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
describe 'scopes' do
|
|
375
|
+
before { register_image_and_other_types }
|
|
376
|
+
|
|
377
|
+
let!(:image) { attach_png(caption: 'a picture', title: 'Pixel') }
|
|
378
|
+
|
|
379
|
+
describe '.latest' do
|
|
380
|
+
it 'includes recently created assets up to the limit' do
|
|
381
|
+
expect(described_class.latest(5)).to include(image)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
describe '.of_types' do
|
|
386
|
+
it 'includes assets whose blob content type matches the given types' do
|
|
387
|
+
expect(described_class.of_types([:image])).to include(image)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
it 'returns none when the given types have no mime types' do
|
|
391
|
+
expect(described_class.of_types([:no_such_type])).to be_empty
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
describe '.matching' do
|
|
396
|
+
it 'matches on filename, title or caption, case-insensitively' do
|
|
397
|
+
expect(described_class.matching('PIXEL')).to include(image)
|
|
398
|
+
expect(described_class.matching('picture')).to include(image)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
describe '.excepting' do
|
|
403
|
+
it 'excludes the given asset ids' do
|
|
404
|
+
expect(described_class.excepting([image.id]).to_sql).to match(/NOT IN/)
|
|
405
|
+
expect(described_class.excepting([image.id])).not_to include(image)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
it 'adds no exclusion when given an empty list' do
|
|
409
|
+
expect(described_class.excepting([])).to eq({})
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
describe '#attached_to?' do
|
|
415
|
+
before { register_image_and_other_types }
|
|
416
|
+
|
|
417
|
+
it 'is true for a page the asset is attached to, false otherwise' do
|
|
418
|
+
asset = attach_png
|
|
419
|
+
page = FactoryBot.create(:page, title: 'Attached')
|
|
420
|
+
other_page = FactoryBot.create(:page, title: 'Unattached')
|
|
421
|
+
asset.pages << page
|
|
422
|
+
|
|
423
|
+
expect(asset.attached_to?(page)).to be(true)
|
|
424
|
+
expect(asset.attached_to?(other_page)).to be(false)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
describe 'class helpers' do
|
|
429
|
+
before { register_image_and_other_types }
|
|
430
|
+
|
|
431
|
+
it 'exposes the known asset type names' do
|
|
432
|
+
expect(described_class.known_types).to eq(AssetType.known_types)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it 'lists ransackable attributes' do
|
|
436
|
+
expect(described_class.ransackable_attributes).to include('title', 'caption', 'uuid')
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it 'exposes the image thumbnail sizes and names' do
|
|
440
|
+
expect(described_class.thumbnail_sizes).to eq(AssetType.find(:image).active_storage_styles)
|
|
441
|
+
expect(described_class.thumbnail_names).to eq(described_class.thumbnail_sizes.keys)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it 'builds thumbnail options with an Original entry first' do
|
|
445
|
+
expect(described_class.thumbnail_options.first).to eq(['Original (as uploaded)', 'original'])
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
it 'describes hash-style thumbnails as "id: geometry as format"' do
|
|
449
|
+
options = described_class.thumbnail_options
|
|
450
|
+
|
|
451
|
+
expect(options).to include(['icon: 50x50# as png', :icon])
|
|
452
|
+
# Every option is a [description, id] pair with a string description.
|
|
453
|
+
expect(options).to all(satisfy { |desc, _id| desc.is_a?(String) })
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it 'represents "original" only once, via the explicit entry (no blank :original option)' do
|
|
457
|
+
ids = described_class.thumbnail_options.map(&:last)
|
|
458
|
+
|
|
459
|
+
expect(ids).to include('original') # the explicit "Original (as uploaded)" entry
|
|
460
|
+
expect(ids).not_to include(:original) # not the blank one built from the :original style
|
|
461
|
+
expect(ids.count { |id| id.to_s == 'original' }).to eq(1)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
describe '.describe_style' do
|
|
466
|
+
it 'renders a hash style as geometry and format' do
|
|
467
|
+
expect(described_class.describe_style(geometry: '100x100#', format: :png)).to eq('100x100# as png')
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
it 'omits a missing format' do
|
|
471
|
+
expect(described_class.describe_style(geometry: '640x640>')).to eq('640x640>')
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
it 'renders an empty hash as a blank string' do
|
|
475
|
+
expect(described_class.describe_style({})).to eq('')
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
it 'joins an array style with " as "' do
|
|
479
|
+
expect(described_class.describe_style(['100x100', 'png'])).to eq('100x100 as png')
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
it 'stringifies a plain style' do
|
|
483
|
+
expect(described_class.describe_style('100x100#')).to eq('100x100#')
|
|
484
|
+
end
|
|
485
|
+
end
|
|
262
486
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe AssetType do
|
|
4
|
+
# AssetType keeps its registry in class variables, so registrations persist
|
|
5
|
+
# for the whole suite. Register uniquely-named types (guarded by .known?) so
|
|
6
|
+
# these examples don't depend on what other specs have registered.
|
|
7
|
+
before(:all) do
|
|
8
|
+
AssetType.new(:other, icon: 'unknown') unless AssetType.known?(:other)
|
|
9
|
+
unless AssetType.known?(:spec_image)
|
|
10
|
+
AssetType.new(:spec_image,
|
|
11
|
+
icon: 'image',
|
|
12
|
+
styles: :standard,
|
|
13
|
+
extensions: %w[spx],
|
|
14
|
+
mime_types: %w[image/spectest])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:image) { AssetType.find(:spec_image) }
|
|
19
|
+
|
|
20
|
+
describe '#plural' do
|
|
21
|
+
it 'pluralizes the name' do
|
|
22
|
+
expect(image.plural).to eq('spec_images')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#mime_types' do
|
|
27
|
+
it 'returns the configured mime types' do
|
|
28
|
+
expect(image.mime_types).to eq(%w[image/spectest])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#condition' do
|
|
33
|
+
it 'builds an IN clause over its mime types' do
|
|
34
|
+
sql, *values = image.condition
|
|
35
|
+
expect(sql).to match(/asset_content_type IN/)
|
|
36
|
+
expect(values).to eq(%w[image/spectest])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#non_condition' do
|
|
41
|
+
it 'builds a NOT IN clause over its mime types' do
|
|
42
|
+
sql, *values = image.non_condition
|
|
43
|
+
expect(sql).to match(/NOT asset_content_type IN/)
|
|
44
|
+
expect(values).to eq(%w[image/spectest])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#standard_styles' do
|
|
49
|
+
it 'defines icon, thumbnail and original styles' do
|
|
50
|
+
expect(image.standard_styles.keys).to match_array(%i[icon thumbnail original])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#normalize_style_rules' do
|
|
55
|
+
it 'wraps a bare geometry string in a hash' do
|
|
56
|
+
expect(image.normalize_style_rules(small: '100x100')).to eq(small: { geometry: '100x100' })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'parses a key=value rule string into a hash' do
|
|
60
|
+
result = image.normalize_style_rules(big: 'geometry=640x640,format=jpg')
|
|
61
|
+
expect(result[:big]).to eq(geometry: '640x640', format: 'jpg')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '.known?' do
|
|
66
|
+
it 'is true for a registered type' do
|
|
67
|
+
expect(AssetType.known?(:spec_image)).to be(true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'is false for an unregistered type' do
|
|
71
|
+
expect(AssetType.known?(:nonexistent)).to be(false)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '.find and .[]' do
|
|
76
|
+
it 'looks up a type by name' do
|
|
77
|
+
expect(AssetType.find(:spec_image)).to eq(image)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'aliases .[] to .find' do
|
|
81
|
+
expect(AssetType[:spec_image]).to eq(image)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns nil for an unknown type' do
|
|
85
|
+
expect(AssetType.find(:nonexistent)).to be_nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '.from_extension' do
|
|
90
|
+
it 'finds a type by a registered extension' do
|
|
91
|
+
expect(AssetType.from_extension('spx')).to eq(image)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '.from_mimetype' do
|
|
96
|
+
it 'finds a type by a registered mime type' do
|
|
97
|
+
expect(AssetType.from_mimetype('image/spectest')).to eq(image)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '.for' do
|
|
102
|
+
it 'returns the catchall type when there is no attachment' do
|
|
103
|
+
expect(AssetType.for(nil)).to eq(AssetType.catchall)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '.catchall' do
|
|
108
|
+
it 'is the :other type' do
|
|
109
|
+
expect(AssetType.catchall).to eq(AssetType.find(:other))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '.all and .known_types' do
|
|
114
|
+
it 'includes registered types' do
|
|
115
|
+
expect(AssetType.known_types).to include(:spec_image)
|
|
116
|
+
expect(AssetType.all).to include(image)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '.known_mimetypes' do
|
|
121
|
+
it 'includes registered mime types' do
|
|
122
|
+
expect(AssetType.known_mimetypes).to include('image/spectest')
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe '.mime_types_for' do
|
|
127
|
+
it 'returns the mime types for the named types' do
|
|
128
|
+
expect(AssetType.mime_types_for(:spec_image)).to eq(%w[image/spectest])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe '.slice' do
|
|
133
|
+
it 'returns the named types' do
|
|
134
|
+
expect(AssetType.slice(:spec_image)).to eq([image])
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe FileNotFoundPage do
|
|
4
|
+
subject(:page) { FileNotFoundPage.new }
|
|
5
|
+
|
|
6
|
+
it 'is a Page' do
|
|
7
|
+
expect(page).to be_a(Page)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '#cache_timeout' do
|
|
11
|
+
it 'is five minutes' do
|
|
12
|
+
expect(page.cache_timeout).to eq(5.minutes)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#allowed_children' do
|
|
17
|
+
it 'has none' do
|
|
18
|
+
expect(page.allowed_children).to eq([])
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#virtual?' do
|
|
23
|
+
it 'is true' do
|
|
24
|
+
expect(page.virtual?).to be(true)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#response_code' do
|
|
29
|
+
it 'is 404' do
|
|
30
|
+
expect(page.response_code).to eq(404)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe HamlFilter do
|
|
4
|
+
subject(:filter) { HamlFilter.new }
|
|
5
|
+
|
|
6
|
+
it 'is a TextFilter' do
|
|
7
|
+
expect(filter).to be_a(TextFilter)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '.filter_name' do
|
|
11
|
+
it 'is derived from the class name' do
|
|
12
|
+
expect(HamlFilter.filter_name).to eq('Haml')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#filter' do
|
|
17
|
+
it 'renders Haml markup to HTML' do
|
|
18
|
+
expect(filter.filter('%p Hello world')).to eq("<p>Hello world</p>\n")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'un-escapes radius tags that Haml would otherwise escape' do
|
|
22
|
+
# Haml escapes the `=` output to <r:title />; the filter restores it.
|
|
23
|
+
expect(filter.filter('= "<r:title />"')).to eq("<r:title/>\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'leaves radius tags in plain text intact' do
|
|
27
|
+
expect(filter.filter("%p\n <r:title />")).to eq("<p>\n<r:title />\n</p>\n")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe MenuRenderer do
|
|
4
|
+
# MenuRenderer is extended onto a Page instance (see Admin::NodeHelper).
|
|
5
|
+
let(:page) { Page.new.tap { |p| p.extend(MenuRenderer) } }
|
|
6
|
+
|
|
7
|
+
# MenuRenderer keeps its exclusion list in a module-level ivar. Isolate it so
|
|
8
|
+
# the .exclude example doesn't leak into others regardless of run order.
|
|
9
|
+
before(:all) { @orig_excluded = MenuRenderer.instance_variable_get(:@excluded_class_names) }
|
|
10
|
+
after(:all) { MenuRenderer.instance_variable_set(:@excluded_class_names, @orig_excluded) }
|
|
11
|
+
before { MenuRenderer.instance_variable_set(:@excluded_class_names, []) }
|
|
12
|
+
|
|
13
|
+
describe '#view' do
|
|
14
|
+
it 'reads back an assigned view' do
|
|
15
|
+
view = Object.new
|
|
16
|
+
page.view = view
|
|
17
|
+
expect(page.view).to eq(view)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#menu_renderer_module_name' do
|
|
22
|
+
it 'is MenuRenderer for a plain page' do
|
|
23
|
+
expect(page.menu_renderer_module_name).to eq('MenuRenderer')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#additional_menu_features?' do
|
|
28
|
+
it 'is false when no page-specific renderer module exists' do
|
|
29
|
+
expect(page.additional_menu_features?).to be_falsey
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '#allowed_child_classes' do
|
|
34
|
+
it 'constantizes the names from the allowed-children cache' do
|
|
35
|
+
page.allowed_children_cache = 'Page,FileNotFoundPage'
|
|
36
|
+
expect(page.allowed_child_classes).to eq([Page, FileNotFoundPage])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'skips names that cannot be constantized' do
|
|
40
|
+
page.allowed_children_cache = 'Page,NotARealClassName'
|
|
41
|
+
expect(page.allowed_child_classes).to eq([Page])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'excludes names registered via .exclude' do
|
|
45
|
+
MenuRenderer.exclude('FileNotFoundPage')
|
|
46
|
+
page.allowed_children_cache = 'Page,FileNotFoundPage'
|
|
47
|
+
expect(page.allowed_child_classes).to eq([Page])
|
|
48
|
+
expect(MenuRenderer.excluded_class_names).to include('FileNotFoundPage')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#add_child_disabled?' do
|
|
53
|
+
it 'is true when there are no allowed child classes' do
|
|
54
|
+
page.allowed_children_cache = ''
|
|
55
|
+
expect(page.add_child_disabled?).to be(true)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'is false when there is at least one allowed child class' do
|
|
59
|
+
page.allowed_children_cache = 'Page'
|
|
60
|
+
expect(page.add_child_disabled?).to be(false)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|