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,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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe PageAttachment do
|
|
4
|
+
describe '#selected?' do
|
|
5
|
+
it 'is false by default' do
|
|
6
|
+
expect(PageAttachment.new.selected?).to be(false)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'is true when selected is set to a truthy value' do
|
|
10
|
+
attachment = PageAttachment.new
|
|
11
|
+
attachment.selected = '1'
|
|
12
|
+
expect(attachment.selected?).to be(true)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'is false when selected is set to nil' do
|
|
16
|
+
attachment = PageAttachment.new
|
|
17
|
+
attachment.selected = nil
|
|
18
|
+
expect(attachment.selected?).to be(false)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#add_to_list_bottom' do
|
|
23
|
+
it 'keeps an already-assigned position' do
|
|
24
|
+
attachment = PageAttachment.new(position: 5)
|
|
25
|
+
attachment.add_to_list_bottom
|
|
26
|
+
expect(attachment.position).to eq(5)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe PageContext do
|
|
4
|
+
let(:page) { FactoryBot.build(:page, title: 'My Page') }
|
|
5
|
+
let(:context) { PageContext.new(page) }
|
|
6
|
+
|
|
7
|
+
describe '#initialize' do
|
|
8
|
+
it 'exposes the page it was built for' do
|
|
9
|
+
expect(context.page).to eq(page)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'registers the page as a global' do
|
|
13
|
+
expect(context.globals.page).to eq(page)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines a tag for each of the page tags' do
|
|
17
|
+
expect(context.definitions.keys).to include('title')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#dup' do
|
|
22
|
+
it 'returns an independent PageContext for the same page' do
|
|
23
|
+
copy = context.dup
|
|
24
|
+
expect(copy).to be_a(PageContext)
|
|
25
|
+
expect(copy).not_to equal(context)
|
|
26
|
+
expect(copy.page).to eq(page)
|
|
27
|
+
expect(copy.globals.page).to eq(page)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'copies the definitions rather than sharing them' do
|
|
31
|
+
copy = context.dup
|
|
32
|
+
expect(copy.definitions).not_to equal(context.definitions)
|
|
33
|
+
expect(copy.definitions.keys).to eq(context.definitions.keys)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe 'rendering through Page#parse' do
|
|
38
|
+
it 'renders a standard tag' do
|
|
39
|
+
expect(page.send(:parse, '<r:title />')).to eq('My Page')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context 'when a tag raises and errors are suppressed (production-like)' do
|
|
43
|
+
before { allow_any_instance_of(PageContext).to receive(:raise_errors?).and_return(false) }
|
|
44
|
+
|
|
45
|
+
it 'renders the error message inside a div' do
|
|
46
|
+
output = page.send(:parse, '<r:find />')
|
|
47
|
+
expect(output).to match(%r{<div><strong>.*find.*</strong></div>})
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when errors are raised (development/test)' do
|
|
52
|
+
it 'propagates the exception' do
|
|
53
|
+
expect { page.send(:parse, '<r:find />') }.to raise_error(StandardTags::TagError)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe PageField do
|
|
4
|
+
describe 'validations' do
|
|
5
|
+
it 'requires a name' do
|
|
6
|
+
field = PageField.new(content: 'value')
|
|
7
|
+
expect(field).not_to be_valid
|
|
8
|
+
expect(field.errors[:name]).to be_present
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'is valid with a name' do
|
|
12
|
+
expect(PageField.new(name: 'keywords', content: 'value')).to be_valid
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|