trek 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.slim-lint.yml +0 -3
  3. data/AGENTS.md +8 -1
  4. data/CHANGELOG.md +62 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +20 -3
  7. data/Makefile +11 -9
  8. data/README.md +1 -1
  9. data/app/assets/icons/trek/chevron-down.svg +1 -0
  10. data/app/assets/icons/trek/chevron-up.svg +1 -0
  11. data/app/assets/icons/trek/external.svg +1 -0
  12. data/app/assets/icons/trek/theme-auto.svg +1 -0
  13. data/app/assets/icons/trek/theme-dark.svg +1 -0
  14. data/app/assets/icons/trek/theme-light.svg +1 -0
  15. data/app/assets/stylesheets/trek/base.css +13 -0
  16. data/app/components/trek/button_component/button_component.css +9 -0
  17. data/app/components/trek/button_component.rb +13 -1
  18. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +1 -1
  19. data/app/components/trek/dashboard/tile_component.rb +14 -0
  20. data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
  21. data/app/components/trek/dialog_component/dialog_component.js +62 -2
  22. data/app/components/trek/dropdown_component/dropdown_component.css +66 -0
  23. data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
  24. data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
  25. data/app/components/trek/dropdown_component.rb +29 -0
  26. data/app/components/trek/form/group_component/group_component.css +34 -0
  27. data/app/components/trek/header_component/header_component.css +10 -4
  28. data/app/components/trek/header_component/header_component.html.slim +17 -10
  29. data/app/components/trek/header_component/header_component.js +29 -5
  30. data/app/components/trek/header_component.rb +20 -3
  31. data/app/components/trek/menu_component/menu_component.css +6 -0
  32. data/app/components/trek/menu_component.rb +29 -4
  33. data/app/components/trek/toaster/toast_component.rb +1 -1
  34. data/app/controllers/concerns/trek/authorization.rb +12 -0
  35. data/app/helpers/trek/page_path_helper.rb +4 -3
  36. data/app/helpers/trek/theme_helper.rb +16 -1
  37. data/app/models/concerns/trek/phonable.rb +15 -3
  38. data/app/models/concerns/trek/sectionable.rb +5 -0
  39. data/app/models/concerns/trek/users/validations.rb +1 -1
  40. data/app/uploaders/audio_uploader.rb +11 -0
  41. data/app/uploaders/record_image_uploader.rb +3 -0
  42. data/config/locales/trek.en.yml +3 -2
  43. data/config/locales/trek.fr.yml +1 -0
  44. data/docs/customize.md +18 -2
  45. data/docs/deploy.md +7 -2
  46. data/docs/getting-started.md +1 -1
  47. data/docs/index.md +1 -1
  48. data/docs/reference/components.md +15 -0
  49. data/docs/reference/concerns/phonable.md +27 -1
  50. data/docs/reference/environment-variables.md +2 -0
  51. data/docs/reference/forms.md +2 -0
  52. data/docs/reference/generators/install.md +4 -1
  53. data/docs/reference/icons.md +1 -1
  54. data/docs/reference/models/tag.md +1 -1
  55. data/docs/reference/models/user.md +4 -2
  56. data/docs/reference/uploaders.md +31 -6
  57. data/docs/what-is-trek.md +1 -1
  58. data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
  59. data/lib/generators/trek/install/appsignal_generator.rb +102 -0
  60. data/lib/generators/trek/install/model_specs_generator.rb +64 -0
  61. data/lib/generators/trek/install/postmark_generator.rb +3 -0
  62. data/lib/generators/trek/install/ruby_generator.rb +1 -1
  63. data/lib/generators/trek/install/scalingo_generator.rb +1 -1
  64. data/lib/generators/trek/install/uploaders_generator.rb +2 -0
  65. data/lib/generators/trek/install_generator.rb +5 -0
  66. data/lib/generators/trek/taxonomies_generator.rb +7 -0
  67. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  68. data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
  69. data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
  70. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
  71. data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
  72. data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
  73. data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
  74. data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
  75. data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
  76. data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
  77. data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
  78. data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
  79. data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
  80. data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
  81. data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
  82. data/lib/generators/trek/templates/models/tag.rb +6 -1
  83. data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
  84. data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
  85. data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
  86. data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
  87. data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
  88. data/lib/generators/trek/templates/spec/models/user_spec.rb +76 -0
  89. data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
  90. data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
  91. data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
  92. data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
  93. data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
  94. data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
  95. data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
  96. data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
  97. data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
  98. data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
  99. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +71 -0
  100. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +45 -0
  101. data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
  102. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
  103. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
  104. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
  105. data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
  106. data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
  107. data/lib/tasks/trek/attachments.rake +18 -0
  108. data/lib/trek/cached_attachments.rb +53 -0
  109. data/lib/trek/saved_attachments_reload.rb +44 -0
  110. data/lib/trek/version.rb +1 -1
  111. data/lib/trek.rb +3 -0
  112. data/package.json +1 -1
  113. data/postcss.config.js +36 -0
  114. data/trek.gemspec +5 -0
  115. metadata +70 -3
  116. data/app/assets/icons/trek/theme.svg +0 -11
@@ -0,0 +1,37 @@
1
+ # Tags every AppSignal transaction with the area it belongs to and the acting user,
2
+ # so errors and slow requests can be filtered per namespace and per actor.
3
+ #
4
+ # Included in ApplicationController (front-office and Admin::BaseController through
5
+ # it) and, from config/initializers/appsignal_context.rb, in Trek::BaseController.
6
+ #
7
+ # Free of charge when AppSignal is inactive (no push API key, i.e. every environment
8
+ # but production): each Appsignal call below returns early.
9
+ module AppsignalContext
10
+ extend ActiveSupport::Concern
11
+
12
+ ADMIN_CONTROLLER_PATHS = ["admin/", "trek/"].freeze
13
+
14
+ included do
15
+ before_action :set_appsignal_context
16
+ end
17
+
18
+ private
19
+
20
+ def set_appsignal_context
21
+ Appsignal.set_namespace(appsignal_namespace)
22
+ Appsignal.add_tags(user_id: appsignal_user_id) if appsignal_user_id
23
+ end
24
+
25
+ def appsignal_namespace
26
+ controller_path.start_with?(*ADMIN_CONTROLLER_PATHS) ? "admin" : "web"
27
+ end
28
+
29
+ def appsignal_user_id
30
+ # Trek::Authentication resolves the session in its own before_action, which may
31
+ # run after this one, so resume it explicitly. Front-office controllers don't
32
+ # include the concern and have no session to resume.
33
+ resume_session if respond_to?(:resume_session, true) && Current.session.nil?
34
+
35
+ Current.user&.id
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ # Makes `fragments` available to seeds and specs: Oaken only defines a model's method once a seed
2
+ # file has used it.
3
+ register Fragment
@@ -0,0 +1,6 @@
1
+ # Defaults for `users.create` and `users.new` (see https://github.com/kaspth/oaken): a unique email and
2
+ # a password passing Trek's validations, so a seed or a spec only sets what it cares about. Specs
3
+ # sign in with `users.password`.
4
+ def users.password = "correct horse battery staple"
5
+
6
+ users.defaults email: -> { "user-#{SecureRandom.hex(4)}@example.com" }, password: users.password
@@ -0,0 +1,5 @@
1
+ # A small page tree for specs, loaded with `seed "cases/page_tree"`: the home page (the tree's root,
2
+ # which has no slug), a section, and a page inside it.
3
+ home = pages.create :home, key: "root", title: "Home"
4
+ company = pages.create :company, title: "Company", parent: home
5
+ pages.create :team, title: "Our team", parent: company
@@ -0,0 +1,5 @@
1
+ # Trek::BaseController is an engine class, so the concern is mixed in on every code
2
+ # reload rather than from a class body the application owns.
3
+ Rails.application.config.to_prepare do
4
+ Trek::BaseController.include(AppsignalContext)
5
+ end
@@ -8,5 +8,5 @@ en:
8
8
  attributes:
9
9
  <%= file_path %>:
10
10
  <%- attributes.each do |attr| -%>
11
- <%= attr.name %>: <%= attr.name.humanize -%>
11
+ <%= attr.name %>: <%= attr.name.humanize %>
12
12
  <%- end -%>
@@ -5,7 +5,7 @@ class TrekFragments < <%= migration_class_name %>
5
5
  t.string :namespace, index: true
6
6
  t.integer :position, null: false
7
7
  t.jsonb :title, default: {}
8
- t.jsonb :content
8
+ t.jsonb :content, default: {}
9
9
 
10
10
  t.timestamps
11
11
 
@@ -18,4 +18,3 @@ class TrekMenuNodes < <%= migration_class_name %>
18
18
  end
19
19
  end
20
20
  end
21
-
@@ -1,6 +1,6 @@
1
1
  class TrekTagCategories < <%= migration_class_name %>
2
2
  def change
3
- create_table "tag_categories" do |t|
3
+ create_table "tag_categories" do |t|
4
4
  t.string :key, index: { unique: true }
5
5
  t.string :name
6
6
  t.integer :position, null: false
@@ -1,6 +1,6 @@
1
1
  class TrekTaggings < <%= migration_class_name %>
2
2
  def change
3
- create_table :taggings do |t|
3
+ create_table :taggings do |t|
4
4
  t.references :taggable, null: false, polymorphic: true
5
5
  t.references :tag, null: false, foreign_key: true
6
6
 
@@ -4,7 +4,8 @@ class TrekUsers < <%= migration_class_name %>
4
4
  add_column :<%= tableized_model_class %>, :theme, :integer, default: 0, null: false
5
5
  add_column :<%= tableized_model_class %>, :locale, :string
6
6
  add_column :<%= tableized_model_class %>, :role, :integer, default: 1, null: false
7
- add_column :<%= tableized_model_class %>, :password_digest, :string, null: false
7
+ # The table is created empty by the trek_create_users migration, so no default is needed.
8
+ add_column :<%= tableized_model_class %>, :password_digest, :string, null: false # rubocop:disable Rails/NotNullColumn
8
9
  add_column :<%= tableized_model_class %>, :last_activity_at, :datetime
9
10
  end
10
11
  end
@@ -6,7 +6,6 @@ class Tag < ApplicationRecord
6
6
  optional: true
7
7
 
8
8
  has_many :taggings, dependent: :destroy
9
- has_many :taggables, through: :taggings
10
9
 
11
10
  validates :name, :key, presence: true
12
11
  validates :key, uniqueness: { scope: :category }
@@ -19,6 +18,12 @@ class Tag < ApplicationRecord
19
18
  scope :in_categories, ->(keys) { TagCategory.where(key: keys).ordered.map(&:tags).reduce(:+) }
20
19
  scope :in_category, ->(key) { TagCategory.find_by(key:)&.tags }
21
20
 
21
+ # The tagged records, of any model. Not a `has_many :through`: Rails can't go through the
22
+ # polymorphic `Tagging#taggable`.
23
+ def taggables
24
+ taggings.includes(:taggable).map(&:taggable)
25
+ end
26
+
22
27
  def to_s
23
28
  name
24
29
  end
@@ -13,7 +13,7 @@ module Admin
13
13
  end
14
14
 
15
15
  def destroy?
16
- user.privileged? && record.taggables.empty?
16
+ user.privileged? && record.taggings.empty?
17
17
  end
18
18
 
19
19
  def reorder?
@@ -16,7 +16,7 @@ module Admin
16
16
  relation_scope do |relation|
17
17
  next relation if user.privileged?
18
18
 
19
- relation.where(user:)
19
+ relation.none
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,24 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Page do
4
+ before { seed "cases/page_tree" }
5
+
6
+ it "slugs the title" do
7
+ expect(pages.team.slug).to eq("our-team")
8
+ end
9
+
10
+ it "builds the full path from the slugs of its ancestors, root excluded" do
11
+ expect(pages.team.full_path).to eq("company/our-team")
12
+ end
13
+
14
+ it "is found at its full path" do
15
+ expect(described_class.find_at_full_path("company/our-team")).to eq(pages.team)
16
+ end
17
+
18
+ it "keeps its former path when moved, so old URLs can be redirected" do
19
+ team = pages.team
20
+ team.update!(parent: pages.home)
21
+
22
+ expect(team.reload.paths.map(&:path)).to eq(["our-team", "company/our-team"])
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe User do
4
+ subject(:user) { users.new }
5
+
6
+ it "is valid with an email and a password" do
7
+ expect(user).to be_valid
8
+ end
9
+
10
+ describe "validations" do
11
+ it "requires an email" do
12
+ user.email = nil
13
+
14
+ expect(user).to be_invalid
15
+ end
16
+
17
+ it "requires a well-formed email" do
18
+ user.email = "not an email"
19
+
20
+ expect(user).to be_invalid
21
+ end
22
+
23
+ it "requires a unique email" do
24
+ users.create(email: user.email)
25
+
26
+ expect(user).to be_invalid
27
+ end
28
+
29
+ it "requires a password of at least 8 characters on creation" do
30
+ user.password = "short"
31
+
32
+ expect(user).to be_invalid
33
+ end
34
+
35
+ it "requires a password of at least 8 characters when changing it" do
36
+ user = users.create
37
+ user.password = "short"
38
+
39
+ expect(user).to be_invalid
40
+ end
41
+
42
+ it "does not require the password when changing something else" do
43
+ user = described_class.find(users.create.id)
44
+ user.name = "Ada"
45
+
46
+ expect(user).to be_valid
47
+ end
48
+ end
49
+
50
+ describe "roles" do
51
+ it "defaults to guest" do
52
+ expect(user.role).to eq("guest")
53
+ end
54
+
55
+ it "treats admins and editors as privileged" do
56
+ expect(%w[admin editor guest].map { |role| users.new(role:).privileged? }).to eq([true, true, false])
57
+ end
58
+
59
+ it "scopes privileged users" do
60
+ admin, editor = users.create(role: :admin), users.create(role: :editor)
61
+ users.create(role: :reader)
62
+
63
+ expect(described_class.privileged).to contain_exactly(admin, editor)
64
+ end
65
+ end
66
+
67
+ describe "#to_s" do
68
+ it "reads the name" do
69
+ expect(users.new(name: "Ada").to_s).to eq("Ada")
70
+ end
71
+
72
+ it "falls back to the email" do
73
+ expect(users.new(email: "ada@example.com").to_s).to eq("ada@example.com")
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,17 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::ExternalLinkPolicy do
4
+ before { ExternalLink.create!(url: "https://example.com") }
5
+
6
+ it "shows privileged users every external link" do
7
+ policy = described_class.new(user: users.new(role: :editor))
8
+
9
+ expect(policy.apply_scope(ExternalLink.all, type: :active_record_relation)).to match_array(ExternalLink.all)
10
+ end
11
+
12
+ it "shows other roles no external link" do
13
+ policy = described_class.new(user: users.new(role: :reader))
14
+
15
+ expect(policy.apply_scope(ExternalLink.all, type: :active_record_relation)).to be_empty
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::FragmentPolicy do
4
+ def policy(role, record = Fragment)
5
+ described_class.new(record, user: users.new(role:))
6
+ end
7
+
8
+ %w[admin editor].each do |role|
9
+ it "lets #{role}s list, create, edit and delete fragments" do
10
+ expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(true))
11
+ end
12
+ end
13
+
14
+ %w[user reader guest].each do |role|
15
+ it "keeps #{role}s away from fragments" do
16
+ expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(false))
17
+ end
18
+ end
19
+
20
+ it "lets privileged users edit a fragment" do
21
+ expect(policy("editor").permitted_attributes).to contain_exactly(:content, :key, :namespace, :title)
22
+ end
23
+
24
+ it "lets other roles edit nothing" do
25
+ expect(policy("reader").permitted_attributes).to be_empty
26
+ end
27
+
28
+ it "shows other roles no fragment" do
29
+ Fragment.create!(key: "footer", namespace: "layout", title: "Footer")
30
+
31
+ expect(policy("reader").apply_scope(Fragment.all, type: :active_record_relation)).to be_empty
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::PagePolicy do
4
+ def policy(role, record = Page)
5
+ described_class.new(record, user: users.new(role:))
6
+ end
7
+
8
+ %w[admin editor].each do |role|
9
+ it "lets #{role}s list, create, edit and delete pages" do
10
+ expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(true))
11
+ end
12
+ end
13
+
14
+ %w[user reader guest].each do |role|
15
+ it "keeps #{role}s away from pages" do
16
+ expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(false))
17
+ end
18
+ end
19
+
20
+ it "lets privileged users edit a page's content" do
21
+ expect(policy("editor").permitted_attributes).to include(:title, :slug, :description, :content)
22
+ end
23
+
24
+ it "lets other roles edit nothing" do
25
+ expect(policy("reader").permitted_attributes).to be_empty
26
+ end
27
+
28
+ it "shows privileged users every page" do
29
+ seed "cases/page_tree"
30
+
31
+ expect(policy("editor").apply_scope(Page.all, type: :active_record_relation)).to match_array(Page.all)
32
+ end
33
+
34
+ it "shows other roles no page" do
35
+ seed "cases/page_tree"
36
+
37
+ expect(policy("reader").apply_scope(Page.all, type: :active_record_relation)).to be_empty
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::PreviewPolicy do
4
+ %w[admin editor user reader guest].each do |role|
5
+ it "lets #{role}s preview the home page" do
6
+ expect(described_class.new(user: users.new(role:)).apply(:home?)).to be(true)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::TagCategoryPolicy do
4
+ let(:category) { TagCategory.create!(name: "Topics", key: "topics") }
5
+
6
+ def policy(role, record = TagCategory)
7
+ described_class.new(record, user: users.new(role:))
8
+ end
9
+
10
+ %w[admin editor].each do |role|
11
+ it "lets #{role}s list, create and edit tag categories" do
12
+ expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(true))
13
+ end
14
+ end
15
+
16
+ %w[user reader guest].each do |role|
17
+ it "keeps #{role}s away from tag categories" do
18
+ expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(false))
19
+ end
20
+ end
21
+
22
+ it "lets privileged users delete an empty category" do
23
+ expect(policy("editor", category).apply(:destroy?)).to be(true)
24
+ end
25
+
26
+ it "keeps a category with tags from being deleted" do
27
+ Tag.create!(name: "News", key: "news", category:)
28
+
29
+ expect(policy("admin", category.reload).apply(:destroy?)).to be(false)
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::TagPolicy do
4
+ let(:tag) { Tag.create!(name: "News", key: "news") }
5
+
6
+ def policy(role, record = Tag)
7
+ described_class.new(record, user: users.new(role:))
8
+ end
9
+
10
+ %w[admin editor].each do |role|
11
+ it "lets #{role}s list, create and edit tags" do
12
+ expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(true))
13
+ end
14
+ end
15
+
16
+ %w[user reader guest].each do |role|
17
+ it "keeps #{role}s away from tags" do
18
+ expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(false))
19
+ end
20
+ end
21
+
22
+ it "lets privileged users delete an unused tag" do
23
+ expect(policy("editor", tag).apply(:destroy?)).to be(true)
24
+ end
25
+
26
+ it "keeps a tag in use from being deleted" do
27
+ allow(tag).to receive(:taggings).and_return([Tagging.new])
28
+
29
+ expect(policy("admin", tag).apply(:destroy?)).to be(false)
30
+ end
31
+ end
@@ -0,0 +1,58 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::UserPolicy do
4
+ let(:admin) { users.create(role: :admin) }
5
+ let(:editor) { users.create(role: :editor) }
6
+
7
+ def policy(user, record = User)
8
+ described_class.new(record, user:)
9
+ end
10
+
11
+ it "lets admins list, create and delete users" do
12
+ expect(%i[index? create? destroy?].map { |rule| policy(admin).apply(rule) }).to all(be(true))
13
+ end
14
+
15
+ it "lets admins edit any user" do
16
+ expect(policy(admin, editor).apply(:update?)).to be(true)
17
+ end
18
+
19
+ it "keeps editors away from the users list" do
20
+ expect(%i[index? create? destroy?].map { |rule| policy(editor).apply(rule) }).to all(be(false))
21
+ end
22
+
23
+ it "lets editors see and edit themselves" do
24
+ expect(%i[show? update?].map { |rule| policy(editor, editor).apply(rule) }).to all(be(true))
25
+ end
26
+
27
+ it "keeps editors away from other users" do
28
+ expect(%i[show? update?].map { |rule| policy(editor, admin).apply(rule) }).to all(be(false))
29
+ end
30
+
31
+ it "lets admins change a user's role" do
32
+ expect(policy(admin, editor).permitted_attributes).to include(:role)
33
+ end
34
+
35
+ it "lets admins invite the users they create" do
36
+ expect(policy(admin, User).permitted_attributes).to include(:send_invite)
37
+ end
38
+
39
+ it "lets editors change their own details but not their role" do
40
+ expect(policy(editor, editor).permitted_attributes).to contain_exactly(:name, :email, :locale, :theme)
41
+ end
42
+
43
+ it "lets editors change nothing on another user" do
44
+ expect(policy(editor, admin).permitted_attributes).to be_empty
45
+ end
46
+
47
+ it "shows admins the privileged users only" do
48
+ reader = users.create(role: :reader)
49
+
50
+ expect(described_class.new(user: admin).apply_scope(User.all, type: :active_record_relation)).not_to include(reader)
51
+ end
52
+
53
+ it "shows editors no user" do
54
+ admin
55
+
56
+ expect(described_class.new(user: editor).apply_scope(User.all, type: :active_record_relation)).to be_empty
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe "Admin fragments", type: :request do
4
+ let!(:footer) { fragments.create(key: "footer", namespace: "layout", title: "Footer") }
5
+
6
+ before do
7
+ editor = users.create(role: :editor)
8
+ post admin_user_session_path, params: { email: editor.email, password: users.password }
9
+ end
10
+
11
+ it "lists the fragments" do
12
+ get admin_fragments_path
13
+
14
+ expect(response.body).to include("footer")
15
+ end
16
+
17
+ it "renders the new fragment form" do
18
+ get new_admin_fragment_path
19
+
20
+ expect(response).to have_http_status(:ok)
21
+ end
22
+
23
+ it "creates a fragment" do
24
+ params = { fragment: { key: "tagline", namespace: "home", title: "Tagline" } }
25
+
26
+ expect { post admin_fragments_path, params: }.to change(Fragment, :count).by(1)
27
+ end
28
+
29
+ it "refuses a key already used in the namespace" do
30
+ post admin_fragments_path, params: { fragment: { key: "footer", namespace: "layout", title: "Another footer" } }
31
+
32
+ expect(response).to have_http_status(:unprocessable_entity)
33
+ end
34
+
35
+ it "renders the edit form" do
36
+ get edit_admin_fragment_path(footer)
37
+
38
+ expect(response).to have_http_status(:ok)
39
+ end
40
+
41
+ it "updates a fragment" do
42
+ patch admin_fragment_path(footer), params: { fragment: { title: "Site footer" } }
43
+
44
+ expect(footer.reload.title).to eq("Site footer")
45
+ end
46
+
47
+ it "deletes a fragment" do
48
+ expect { delete admin_fragment_path(footer) }.to change(Fragment, :count).by(-1)
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe "Admin pages", type: :request do
4
+ before do
5
+ seed "cases/page_tree"
6
+ editor = users.create(role: :editor)
7
+ post admin_user_session_path, params: { email: editor.email, password: users.password }
8
+ end
9
+
10
+ it "lists the pages" do
11
+ get admin_pages_path
12
+
13
+ expect(response.body).to include("Company").and include("Our team")
14
+ end
15
+
16
+ it "renders the new page form" do
17
+ get new_admin_page_path
18
+
19
+ expect(response).to have_http_status(:ok)
20
+ end
21
+
22
+ it "creates a page" do
23
+ expect { post admin_pages_path, params: { page: { title: "Contact" } } }.to change(Page, :count).by(1)
24
+ end
25
+
26
+ it "refuses a page without a title" do
27
+ post admin_pages_path, params: { page: { title: "" } }
28
+
29
+ expect(response).to have_http_status(:unprocessable_entity)
30
+ end
31
+
32
+ it "renders the edit form" do
33
+ get edit_admin_page_path(pages.team)
34
+
35
+ expect(response).to have_http_status(:ok)
36
+ end
37
+
38
+ it "updates a page" do
39
+ patch admin_page_path(pages.team), params: { page: { title: "The team" } }
40
+
41
+ expect(pages.team.title).to eq("The team")
42
+ end
43
+
44
+ it "deletes a page" do
45
+ expect { delete admin_page_path(pages.team) }.to change(Page, :count).by(-1)
46
+ end
47
+ end
@@ -0,0 +1,22 @@
1
+ require "rails_helper"
2
+
3
+ # The endpoints the admin's image and sound fields upload to (`Trek::Form::ImageFieldComponent`,
4
+ # `Trek::Form::SoundFieldComponent`).
5
+ RSpec.describe "Admin uploads", type: :request do
6
+ before do
7
+ editor = users.create(role: :editor)
8
+ post admin_user_session_path, params: { email: editor.email, password: users.password }
9
+ end
10
+
11
+ it "stores an uploaded image in the cache and returns its data" do
12
+ post "/admin/upload/image", params: { file: fixture_file_upload("image.png", "image/png") }
13
+
14
+ expect(response.parsed_body["data"]).to include("id", "storage" => "cache")
15
+ end
16
+
17
+ it "stores an uploaded sound in the cache and returns its data" do
18
+ post "/admin/upload/audio", params: { file: fixture_file_upload("sound.mp3", "audio/mpeg") }
19
+
20
+ expect(response.parsed_body["data"]).to include("id", "storage" => "cache")
21
+ end
22
+ end