wor-simple_crud 0.5.0 → 0.7.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/Gemfile +5 -1
  4. data/README.md +104 -8
  5. data/lib/simple_crud/action_context.rb +45 -10
  6. data/lib/simple_crud/create_context.rb +2 -1
  7. data/lib/simple_crud/destroy_context.rb +2 -1
  8. data/lib/simple_crud/index_context.rb +30 -12
  9. data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -1
  10. data/lib/simple_crud/persistence_context.rb +33 -6
  11. data/lib/simple_crud/relation_source.rb +107 -0
  12. data/lib/simple_crud/rspec/config.rb +6 -1
  13. data/lib/simple_crud/rspec/helpers/authentication.rb +2 -1
  14. data/lib/simple_crud/rspec/helpers/controller_options.rb +2 -2
  15. data/lib/simple_crud/rspec/helpers/requests.rb +5 -2
  16. data/lib/simple_crud/rspec.rb +2 -0
  17. data/lib/simple_crud/serializer.rb +14 -0
  18. data/lib/simple_crud/show_context.rb +3 -1
  19. data/lib/simple_crud/simple_crud_controller.rb +29 -6
  20. data/lib/simple_crud/update_context.rb +2 -1
  21. data/lib/simple_crud/version.rb +1 -1
  22. data/lib/spec/matchers/have_been_serialized_with.rb +19 -2
  23. data/lib/spec/shared_examples/simple_crud_for_create.rb +6 -6
  24. data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +2 -2
  25. data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +1 -1
  26. data/lib/spec/shared_examples/simple_crud_for_destroy.rb +8 -8
  27. data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +1 -1
  28. data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +3 -3
  29. data/lib/spec/shared_examples/simple_crud_for_edit.rb +2 -2
  30. data/lib/spec/shared_examples/simple_crud_for_index.rb +3 -3
  31. data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +1 -1
  32. data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +1 -1
  33. data/lib/spec/shared_examples/simple_crud_for_nested_resource.rb +21 -0
  34. data/lib/spec/shared_examples/simple_crud_for_new.rb +1 -1
  35. data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +1 -1
  36. data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +1 -1
  37. data/lib/spec/shared_examples/simple_crud_for_owned_resource.rb +35 -0
  38. data/lib/spec/shared_examples/simple_crud_for_show.rb +2 -2
  39. data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +1 -1
  40. data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +1 -1
  41. data/lib/spec/shared_examples/simple_crud_for_update.rb +7 -7
  42. data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +1 -1
  43. data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +1 -1
  44. data/spec/blueprint/dummy_models_controller_spec.rb +25 -0
  45. data/spec/blueprint_unpaginated/dummy_models_controller_spec.rb +9 -0
  46. data/spec/dummy/app/blueprints/dummy_model_blueprint.rb +10 -0
  47. data/spec/dummy/app/controllers/blueprint/dummy_models_controller.rb +21 -0
  48. data/spec/dummy/app/controllers/blueprint_unpaginated/dummy_models_controller.rb +10 -0
  49. data/spec/dummy/app/controllers/html_notice/dummy_models_controller.rb +11 -0
  50. data/spec/dummy/app/controllers/owned/dummy_models_controller.rb +21 -0
  51. data/spec/dummy/app/controllers/parented/dummy_models_controller.rb +23 -0
  52. data/spec/dummy/app/controllers/scoped_symbol/dummy_models_controller.rb +11 -0
  53. data/spec/dummy/app/models/dummy_model.rb +4 -0
  54. data/spec/dummy/app/serializers/dummy_model_plain_serializer.rb +8 -0
  55. data/spec/dummy/app/views/html_notice/dummy_models/edit.html.erb +1 -0
  56. data/spec/dummy/config/routes.rb +24 -0
  57. data/spec/dummy_model_blueprint_spec.rb +16 -0
  58. data/spec/html_notice/dummy_models_controller_spec.rb +38 -0
  59. data/spec/owned/dummy_models_controller_spec.rb +12 -0
  60. data/spec/parented/dummy_models_controller_spec.rb +29 -0
  61. data/spec/scoped_symbol/dummy_models_controller_spec.rb +7 -0
  62. data/spec/simple_crud/action_context_spec.rb +34 -0
  63. data/spec/simple_crud/index_context_spec.rb +49 -0
  64. data/spec/simple_crud/relation_source_spec.rb +125 -0
  65. data/spec/simple_crud/rspec_config_spec.rb +13 -0
  66. data/spec/simple_crud/serializer_spec.rb +49 -0
  67. data/spec/simple_crud/show_context_spec.rb +5 -0
  68. data/spec/simple_crud_controller_spec.rb +15 -0
  69. metadata +25 -3
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe BlueprintUnpaginated::DummyModelsController, type: :controller do
6
+ let(:serializer) { DummyModelBlueprint }
7
+
8
+ include_examples 'simple crud for index'
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyModelBlueprint < Blueprinter::Base
4
+ identifier :id
5
+ fields :name, :something, :user_id
6
+
7
+ field :url do |record, options|
8
+ options[:url_builder]&.dummy_model_url(record)
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprint
4
+ class DummyModelsController < ApplicationController
5
+ include Wor::Paginate
6
+ include Pundit::Authorization
7
+ extend SimpleCrudController
8
+
9
+ def dummy_model_params
10
+ params.permit(:name, :something, :user_id, :slug)
11
+ end
12
+
13
+ SERIALIZER_OPTIONS = -> { { url_builder: self } }
14
+
15
+ simple_crud_for :index, serializer: DummyModelBlueprint
16
+ simple_crud_for :show, serializer: DummyModelBlueprint, serializer_options: SERIALIZER_OPTIONS
17
+ simple_crud_for :create, serializer: DummyModelBlueprint, serializer_options: SERIALIZER_OPTIONS
18
+ simple_crud_for :update, serializer: DummyModelBlueprint, serializer_options: SERIALIZER_OPTIONS
19
+ simple_crud_for :destroy, serializer: DummyModelBlueprint, serializer_options: SERIALIZER_OPTIONS
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlueprintUnpaginated
4
+ class DummyModelsController < ApplicationController
5
+ include Pundit::Authorization
6
+ extend SimpleCrudController
7
+
8
+ simple_crud_for :index, paginate: false, serializer: DummyModelBlueprint
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HtmlNotice
4
+ class DummyModelsController < HtmlModes::BaseController
5
+ simple_crud_for :create, html: true, authenticate: false, authorize: false,
6
+ notice: 'created', alert: 'invalid'
7
+ simple_crud_for :update, html: true, authenticate: false, authorize: false,
8
+ notice: 'updated', alert: 'invalid'
9
+ simple_crud_for :destroy, html: true, authenticate: false, authorize: false, notice: 'removed'
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Owned
4
+ class DummyModelsController < ApplicationController
5
+ include Wor::Paginate
6
+ include Pundit::Authorization
7
+ extend SimpleCrudController
8
+
9
+ # No :user_id: a permitted FK would let a request override the owner set by owned_by:.
10
+ def dummy_model_params
11
+ params.permit(:name, :something, :slug)
12
+ end
13
+
14
+ simple_crud_defaults owned_by: :dummy_models, authorize: false
15
+ simple_crud_for :index
16
+ simple_crud_for :show
17
+ simple_crud_for :create
18
+ simple_crud_for :update
19
+ simple_crud_for :destroy
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parented
4
+ class DummyModelsController < ApplicationController
5
+ include Wor::Paginate
6
+ include Pundit::Authorization
7
+ extend SimpleCrudController
8
+
9
+ # The default association for this model is @user.dummy_models.
10
+ def owner = @owner ||= User.find(params[:owner_id])
11
+
12
+ def dummy_model_params
13
+ params.permit(:name, :something, :slug)
14
+ end
15
+
16
+ simple_crud_defaults parent: :owner, authorize: false
17
+ simple_crud_for :index
18
+ simple_crud_for :show
19
+ simple_crud_for :create
20
+ simple_crud_for :update
21
+ simple_crud_for :destroy
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScopedSymbol
4
+ class DummyModelsController < ApplicationController
5
+ include Wor::Paginate
6
+ include Pundit::Authorization
7
+ extend SimpleCrudController
8
+
9
+ simple_crud_for :index, scope: :visible_to
10
+ end
11
+ end
@@ -8,4 +8,8 @@ class DummyModel < ApplicationRecord
8
8
  def self.find_by_slug(params)
9
9
  find_by!(slug: params[:slug])
10
10
  end
11
+
12
+ def self.visible_to(user, _params)
13
+ where(user: user)
14
+ end
11
15
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Exercises the plain-class serializer path.
4
+ class DummyModelPlainSerializer
5
+ def self.render(record, **options)
6
+ { id: record.id, name: record.name, something: record.something, root: options[:root] }.compact
7
+ end
8
+ end
@@ -40,6 +40,14 @@ Rails.application.routes.draw do
40
40
  resources :dummy_models, only: :index
41
41
  end
42
42
 
43
+ namespace :owned do
44
+ resources :dummy_models
45
+ end
46
+
47
+ namespace :scoped_symbol do
48
+ resources :dummy_models, only: :index
49
+ end
50
+
43
51
  namespace :unpaginated_scoped do
44
52
  resources :dummy_models, only: :index
45
53
  end
@@ -88,6 +96,10 @@ Rails.application.routes.draw do
88
96
  resources :dummy_models, only: :destroy
89
97
  end
90
98
 
99
+ namespace :html_notice do
100
+ resources :dummy_models, only: %i[create update destroy]
101
+ end
102
+
91
103
  namespace :block_destroy do
92
104
  resources :dummy_models, only: :destroy
93
105
  end
@@ -120,9 +132,21 @@ Rails.application.routes.draw do
120
132
  resources :dummy_models, only: :create
121
133
  end
122
134
 
135
+ namespace :blueprint do
136
+ resources :dummy_models
137
+ end
138
+
139
+ namespace :blueprint_unpaginated do
140
+ resources :dummy_models, only: :index
141
+ end
142
+
123
143
  namespace :nested_route do
124
144
  resources :classrooms, only: [], param: :slug do
125
145
  resources :dummy_models, param: :slug, only: %i[show create destroy]
126
146
  end
127
147
  end
148
+
149
+ namespace :parented do
150
+ resources :dummy_models
151
+ end
128
152
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe DummyModelBlueprint do
6
+ let(:dummy_model) { DummyModel.new(id: 3, name: 'widget', something: 'thing', user_id: 7) }
7
+ let(:full_response) { described_class.render_as_hash(dummy_model) }
8
+
9
+ it 'matches a response including all blueprint attributes' do
10
+ expect(full_response).to have_been_serialized_with(described_class)
11
+ end
12
+
13
+ it 'does not match a response missing blueprint attributes' do
14
+ expect({ 'id' => dummy_model.id }).not_to have_been_serialized_with(described_class)
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe HtmlNotice::DummyModelsController, type: :controller do
6
+ describe 'POST #create' do
7
+ it 'flashes the notice on success' do
8
+ post :create, params: attributes_for(:dummy_model).merge(user_id: create(:user).id), format: :html
9
+
10
+ expect(flash[:notice]).to eq('created')
11
+ end
12
+ end
13
+
14
+ describe 'PUT #update' do
15
+ let(:record) { create(:dummy_model) }
16
+
17
+ it 'flashes the notice on success' do
18
+ put :update, params: { id: record.id, name: 'renamed' }, format: :html
19
+
20
+ expect(flash[:notice]).to eq('updated')
21
+ end
22
+
23
+ it 'flashes the alert on failure' do
24
+ put :update, params: { id: record.id, name: nil }, format: :html
25
+
26
+ expect(flash.now[:alert]).to eq('invalid')
27
+ end
28
+ end
29
+
30
+ describe 'DELETE #destroy' do
31
+ it 'flashes the notice on success' do
32
+ record = create(:dummy_model)
33
+ delete :destroy, params: { id: record.id }, format: :html
34
+
35
+ expect(flash[:notice]).to eq('removed')
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Owned::DummyModelsController, type: :controller do
6
+ include_examples 'simple crud for show'
7
+ include_examples 'simple crud for create'
8
+ include_examples 'simple crud for update'
9
+ include_examples 'simple crud for destroy'
10
+ include_examples 'simple crud for index'
11
+ include_examples 'simple crud for owned resource'
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Parented::DummyModelsController, simple_crud: {
6
+ route_params: -> { { owner_id: current_user.id } }
7
+ }, type: :controller do
8
+ let(:owner) { create(:user) }
9
+
10
+ include_examples 'simple crud for nested resource'
11
+
12
+ describe 'POST #create' do
13
+ include_context 'with authenticated user'
14
+
15
+ it 'builds the record through the parent association' do
16
+ post :create, params: { owner_id: owner.id, name: 'from parent' }, format: :json
17
+
18
+ expect(owner.dummy_models.find(response.parsed_body['id']).name).to eq('from parent')
19
+ end
20
+ end
21
+
22
+ describe 'POST #create with parent: and owned_by: together' do
23
+ it 'raises, they are the same relation source' do
24
+ expect do
25
+ described_class.simple_crud_for(:show, parent: :owner, owned_by: :dummy_models)
26
+ end.to raise_error(ArgumentError, /mutually exclusive/)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ScopedSymbol::DummyModelsController, type: :controller do
6
+ include_examples 'simple crud for index with scope'
7
+ end
@@ -19,4 +19,38 @@ describe SimpleCrud::ActionContext do
19
19
  expect(key).to eq('dummy_model:show:v1:/dummy_models/1')
20
20
  end
21
21
  end
22
+
23
+ describe '#build_record' do
24
+ it 'raises not found when owned_by has no owner to build from' do
25
+ ctx = described_class.new(double(params: {}), DummyModel, { owned_by: :dummy_models })
26
+ expect { ctx.send(:build_record) }.to raise_error(ActiveRecord::RecordNotFound, /no owner/)
27
+ end
28
+
29
+ it 'builds through the scoped relation when configured' do
30
+ relation = double(build: :built)
31
+ ctrl = double(params: {})
32
+ ctrl.instance_variable_set(:@owner, double(dummy_models: relation))
33
+
34
+ expect(described_class.new(ctrl, DummyModel, { parent: :owner }).send(:build_record)).to eq(:built)
35
+ end
36
+ end
37
+
38
+ describe '#serializer_options' do
39
+ let(:record) { DummyModel.new(name: 'widget') }
40
+
41
+ it 'returns an empty hash when no callable is configured' do
42
+ ctx = described_class.new(double, DummyModel, {})
43
+ expect(ctx.send(:serializer_options, record)).to eq({})
44
+ end
45
+
46
+ it 'calls a zero-arity callable without the record' do
47
+ ctx = described_class.new(double, DummyModel, { serializer_options: -> { { prefix: :none } } })
48
+ expect(ctx.send(:serializer_options, record)).to eq(prefix: :none)
49
+ end
50
+
51
+ it 'calls an arity-one callable with the record' do
52
+ ctx = described_class.new(double, DummyModel, { serializer_options: ->(record) { { name: record.name } } })
53
+ expect(ctx.send(:serializer_options, record)).to eq(name: 'widget')
54
+ end
55
+ end
22
56
  end
@@ -27,5 +27,54 @@ describe SimpleCrud::IndexContext do
27
27
  ctx = described_class.new(ctrl, DummyModel, { scope: ->(u) { u }, authorize: false })
28
28
  expect(ctx.send(:call_scope)).to eq(admin)
29
29
  end
30
+
31
+ it 'runs an arity-zero scope with the controller as self and no arguments' do
32
+ ctrl = double(params: {}, listing: DummyModel.none)
33
+ ctx = described_class.new(ctrl, DummyModel, { scope: -> { listing }, authorize: false })
34
+
35
+ expect(ctx.send(:call_scope)).to eq(DummyModel.none)
36
+ end
37
+
38
+ it 'dispatches a Symbol scope to the model with user and params' do
39
+ ctrl = double(params: { status: 'active' })
40
+ allow(DummyModel).to receive(:visible_to).with(nil, { status: 'active' }).and_return(DummyModel.none)
41
+ ctx = described_class.new(ctrl, DummyModel, { scope: :visible_to, authorize: false })
42
+
43
+ expect(ctx.send(:call_scope)).to eq(DummyModel.none)
44
+ end
45
+ end
46
+
47
+ describe '#index_relation' do
48
+ let(:owned_records) { DummyModel.none }
49
+
50
+ it 'scopes to the owned_by relation even when authorize is on' do
51
+ ctrl = double(params: {}, current_user: double(dummy_models: owned_records))
52
+ ctx = described_class.new(ctrl, DummyModel, { owned_by: :dummy_models, authorize: true })
53
+
54
+ expect(ctx.send(:index_relation)).to eq(owned_records)
55
+ end
56
+
57
+ it 'returns no records when there is no user to own them' do
58
+ ctx = described_class.new(double(params: {}), DummyModel, { owned_by: :dummy_models, authorize: false })
59
+
60
+ expect(ctx.send(:index_relation)).to be_empty
61
+ end
62
+
63
+ it 'scopes to the parent relation' do
64
+ parent = double(dummy_models: owned_records)
65
+ ctrl = double(params: {})
66
+ ctrl.instance_variable_set(:@owner, parent)
67
+ ctx = described_class.new(ctrl, DummyModel, { parent: :owner, authorize: false })
68
+
69
+ expect(ctx.send(:index_relation)).to eq(owned_records)
70
+ end
71
+
72
+ it 'returns no records when the parent association is empty' do
73
+ ctrl = double(params: {})
74
+ ctrl.instance_variable_set(:@owner, double(dummy_models: nil))
75
+ ctx = described_class.new(ctrl, DummyModel, { parent: :owner, authorize: false })
76
+
77
+ expect(ctx.send(:index_relation)).to be_empty
78
+ end
30
79
  end
31
80
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SimpleCrud::RelationSource do
6
+ after { SimpleCrud::Config.user_method = :current_user }
7
+
8
+ let(:controller) { double(params: {}) }
9
+ let(:source) { described_class.new(controller, DummyModel, parameters) }
10
+
11
+ describe '#configured?' do
12
+ it 'is true for owned_by' do
13
+ expect(described_class.new(controller, DummyModel, { owned_by: :dummy_models })).to be_configured
14
+ end
15
+
16
+ it 'is true for parent' do
17
+ expect(described_class.new(controller, DummyModel, { parent: :owner })).to be_configured
18
+ end
19
+
20
+ it 'is false when neither is set' do
21
+ expect(described_class.new(controller, DummyModel, {})).not_to be_configured
22
+ end
23
+ end
24
+
25
+ describe '#relation' do
26
+ it 'is nil when neither option is set' do
27
+ expect(described_class.new(controller, DummyModel, {}).relation).to be_nil
28
+ end
29
+
30
+ context 'with owned_by' do
31
+ let(:parameters) { { owned_by: :dummy_models } }
32
+
33
+ it 'resolves the association on the user' do
34
+ models = DummyModel.none
35
+ allow(controller).to receive(:current_user).and_return(double(dummy_models: models))
36
+
37
+ expect(source.relation).to eq(models)
38
+ end
39
+
40
+ it 'is nil without a current user' do
41
+ expect(source.relation).to be_nil
42
+ end
43
+
44
+ it 'resolves the user via overridden Config.user_method' do
45
+ SimpleCrud::Config.user_method = :current_admin
46
+ models = DummyModel.none
47
+ allow(controller).to receive(:current_admin).and_return(double(dummy_models: models))
48
+
49
+ expect(source.relation).to eq(models)
50
+ end
51
+ end
52
+
53
+ context 'with parent' do
54
+ let(:parameters) { { parent: :owner } }
55
+
56
+ it 'prefers a controller method over an ivar' do
57
+ parent = double(dummy_models: DummyModel.none)
58
+ allow(controller).to receive(:owner).and_return(parent)
59
+
60
+ expect(source.relation).to eq(DummyModel.none)
61
+ end
62
+
63
+ it 'falls back to the ivar when there is no method' do
64
+ parent = double(dummy_models: DummyModel.none)
65
+ controller.instance_variable_set(:@owner, parent)
66
+
67
+ expect(source.relation).to eq(DummyModel.none)
68
+ end
69
+
70
+ it 'runs a Proc parent in controller context' do
71
+ parent = double(dummy_models: DummyModel.none)
72
+ allow(controller).to receive(:owner).and_return(parent)
73
+ proc_source = described_class.new(controller, DummyModel, { parent: -> { owner } })
74
+
75
+ expect(proc_source.relation).to eq(DummyModel.none)
76
+ end
77
+
78
+ it 'is nil when the parent resolves to nil' do
79
+ controller.instance_variable_set(:@owner, nil)
80
+
81
+ expect(source.relation).to be_nil
82
+ end
83
+
84
+ it 'derives the association from the model' do
85
+ parent = double
86
+ controller.instance_variable_set(:@owner, parent)
87
+ allow(parent).to receive(:dummy_models).and_return(DummyModel.none)
88
+
89
+ expect(source.relation).to eq(DummyModel.none)
90
+ end
91
+
92
+ it 'honors parent_association:' do
93
+ models = DummyModel.none
94
+ controller.instance_variable_set(:@owner, double(special: models))
95
+ source = described_class.new(controller, DummyModel, { parent: :owner, parent_association: :special })
96
+
97
+ expect(source.relation).to eq(models)
98
+ end
99
+ end
100
+ end
101
+
102
+ describe '#relation!' do
103
+ let(:parameters) { { owned_by: :dummy_models } }
104
+
105
+ it 'raises not found when the owner cannot be resolved' do
106
+ expect { source.relation! }.to raise_error(ActiveRecord::RecordNotFound, /no owner for dummy_models/)
107
+ end
108
+ end
109
+
110
+ describe '#user' do
111
+ let(:parameters) { {} }
112
+
113
+ it 'returns nil when the controller has no user method' do
114
+ expect(source.user).to be_nil
115
+ end
116
+ end
117
+
118
+ describe 'unresolvable parent' do
119
+ let(:parameters) { { parent: :owner } }
120
+
121
+ it 'raises when the parent is neither a method nor an ivar' do
122
+ expect { source.relation }.to raise_error(ArgumentError, /neither a controller method nor an @owner ivar/)
123
+ end
124
+ end
125
+ end
@@ -58,6 +58,19 @@ describe SimpleCrud::RSpec do
58
58
  SimpleCrud::RSpec.configure { |c| c.required_attribute = original }
59
59
  expect(config.required_attribute).to eq(:name)
60
60
  end
61
+
62
+ describe 'the default serializer_class' do
63
+ it 'prefers the AMS serializer when the constant exists' do
64
+ expect(config.serializer_class.call(DummyModel.new)).to eq(DummyModelSerializer)
65
+ end
66
+
67
+ it 'falls back to the Blueprint when there is no AMS serializer' do
68
+ allow(Kernel).to receive(:const_defined?).and_call_original
69
+ allow(Kernel).to receive(:const_defined?).with('DummyModelSerializer').and_return(false)
70
+
71
+ expect(config.serializer_class.call(DummyModel.new)).to eq(DummyModelBlueprint)
72
+ end
73
+ end
61
74
  end
62
75
 
63
76
  describe SimpleCrud::RSpec::Helpers do
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SimpleCrud::Serializer do
6
+ let(:record) { DummyModel.new(id: 1, name: 'widget', something: 'thing', user_id: 9) }
7
+ let(:non_class_serializer) do
8
+ Module.new do
9
+ def self.render(_record, **)
10
+ :plain
11
+ end
12
+
13
+ def self.new(*)
14
+ Struct.new(:as_json).new(:ams)
15
+ end
16
+ end
17
+ end
18
+
19
+ describe '.render' do
20
+ it 'falls back to the record as_json when there is no serializer' do
21
+ expect(described_class.render(nil, record)).to eq(record.as_json)
22
+ end
23
+
24
+ it 'renders through a Blueprinter blueprint with the given options' do
25
+ result = described_class.render(DummyModelBlueprint, record, { url_builder: nil })
26
+
27
+ expect(result).to include(id: record.id, name: record.name, something: record.something, user_id: record.user_id)
28
+ end
29
+
30
+ it 'calls a plain class .render method with the options splatted as keywords' do
31
+ result = described_class.render(DummyModelPlainSerializer, record, { root: false })
32
+
33
+ expect(result).to eq(id: record.id, name: record.name, something: record.something, root: false)
34
+ end
35
+
36
+ it 'falls through to the AMS path for a non-class that responds to render' do
37
+ expect(described_class.render(non_class_serializer, record)).to eq(:ams)
38
+ end
39
+
40
+ it 'instantiates an AMS-style serializer with the given options', :aggregate_failures do
41
+ allow(DummyModelSerializer).to receive(:new).and_call_original
42
+
43
+ result = described_class.render(DummyModelSerializer, record, { root: false })
44
+
45
+ expect(DummyModelSerializer).to have_received(:new).with(record, { root: false })
46
+ expect(result).to include(id: record.id, something: record.something)
47
+ end
48
+ end
49
+ end
@@ -10,5 +10,10 @@ describe SimpleCrud::ShowContext do
10
10
  expect { ctx.send(:find_record) }
11
11
  .to raise_error(ActiveRecord::RecordNotFound, /must return a single record/)
12
12
  end
13
+
14
+ it 'raises not found when owned_by has no owner to scope the lookup to' do
15
+ ctx = described_class.new(double(params: { id: 1 }), DummyModel, { owned_by: :dummy_models, authorize: false })
16
+ expect { ctx.send(:find_record) }.to raise_error(ActiveRecord::RecordNotFound, /no owner/)
17
+ end
13
18
  end
14
19
  end
@@ -16,7 +16,22 @@ end
16
16
  class InheritingController < BaseWithDefaultsController
17
17
  end
18
18
 
19
+ class MultiActionModel; end
20
+
21
+ class MultiActionModelsController
22
+ extend SimpleCrudController
23
+ end
24
+
19
25
  describe SimpleCrudController do
26
+ describe '.simple_crud_for' do
27
+ it 'declares every action when given an array', :aggregate_failures do
28
+ MultiActionModelsController.simple_crud_for(%i[show create], authorize: false, authenticate: false)
29
+
30
+ expect(MultiActionModelsController.method_defined?(:show)).to be true
31
+ expect(MultiActionModelsController.method_defined?(:create)).to be true
32
+ end
33
+ end
34
+
20
35
  describe '.check_policies' do
21
36
  it 'returns when a matching policy exists' do
22
37
  expect { DummyModelsController.check_policies(authorize: true) }.not_to raise_error