wor-simple_crud 0.2.1 → 0.3.1
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/.gitignore +5 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +1 -0
- data/README.md +200 -2
- data/lib/simple_crud/action_lambdas.rb +81 -0
- data/lib/simple_crud/authorization/adapter.rb +7 -0
- data/lib/simple_crud/authorization/pundit_adapter.rb +6 -0
- data/lib/simple_crud/config.rb +6 -1
- data/lib/simple_crud/controller_helpers.rb +128 -0
- data/lib/simple_crud/pagination/adapter.rb +7 -1
- data/lib/simple_crud/pagination/kaminari_adapter.rb +23 -2
- data/lib/simple_crud/pagination/pagy_adapter.rb +7 -2
- data/lib/simple_crud/pagination/will_paginate_adapter.rb +7 -3
- data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -2
- data/lib/simple_crud/rspec/config.rb +74 -0
- data/lib/simple_crud/rspec/helpers/authentication.rb +32 -0
- data/lib/simple_crud/rspec/helpers/controller_options.rb +19 -0
- data/lib/simple_crud/rspec/helpers/models.rb +55 -0
- data/lib/simple_crud/rspec/helpers/policies.rb +25 -0
- data/lib/simple_crud/rspec/helpers/requests.rb +45 -0
- data/lib/simple_crud/rspec/helpers/settings.rb +49 -0
- data/lib/simple_crud/rspec/helpers.rb +25 -0
- data/lib/simple_crud/rspec.rb +35 -0
- data/lib/simple_crud/simple_crud_controller.rb +27 -75
- data/lib/simple_crud/version.rb +1 -1
- data/lib/spec/shared_contexts/authenticate_user.rb +1 -7
- data/lib/spec/shared_examples/authorization_adapter_authorize.rb +11 -0
- data/lib/spec/shared_examples/simple_crud_for_create.rb +46 -11
- data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +36 -0
- data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +15 -0
- data/lib/spec/shared_examples/simple_crud_for_destroy.rb +44 -8
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +21 -0
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +35 -0
- data/lib/spec/shared_examples/simple_crud_for_edit.rb +79 -0
- data/lib/spec/shared_examples/simple_crud_for_index.rb +9 -24
- data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +22 -0
- data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +33 -0
- data/lib/spec/shared_examples/simple_crud_for_new.rb +29 -0
- data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +20 -0
- data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +19 -0
- data/lib/spec/shared_examples/simple_crud_for_show.rb +14 -10
- data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +22 -0
- data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +33 -0
- data/lib/spec/shared_examples/simple_crud_for_update.rb +61 -14
- data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +39 -0
- data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +14 -0
- data/lib/spec/shared_examples/simple_crud_renders_template.rb +8 -0
- data/lib/spec/shared_examples/simple_crud_when_not_authorized.rb +18 -0
- data/lib/spec/shared_examples/simple_crud_without_authenticated_user.rb +9 -0
- data/lib/spec/shared_examples/unauthorized_when_not_logged_in.rb +2 -2
- data/spec/block/dummy_models_controller_spec.rb +8 -0
- data/spec/block_create/dummy_models_controller_spec.rb +7 -0
- data/spec/block_destroy/dummy_models_controller_spec.rb +7 -0
- data/spec/block_finder/dummy_models_controller_spec.rb +10 -0
- data/spec/block_json/dummy_models_controller_spec.rb +9 -0
- data/spec/block_new/dummy_models_controller_spec.rb +7 -0
- data/spec/block_redirect/dummy_models_controller_spec.rb +7 -0
- data/spec/block_show/dummy_models_controller_spec.rb +7 -0
- data/spec/built/dummy_models_controller_spec.rb +8 -0
- data/spec/dummy/app/controllers/block/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/block_create/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/block_destroy/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/block_finder/dummy_models_controller.rb +25 -0
- data/spec/dummy/app/controllers/block_json/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/block_modes/base_controller.rb +11 -0
- data/spec/dummy/app/controllers/block_new/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/block_show/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/built/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/dummy_models_controller.rb +2 -1
- data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +23 -0
- data/spec/dummy/app/controllers/html/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_create/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_destroy/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_finder/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/html_modes/base_controller.rb +17 -0
- data/spec/dummy/app/controllers/html_new/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_scoped/dummy_models_controller.rb +8 -0
- data/spec/dummy/app/controllers/html_show/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_update/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/invalid_status/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/nested/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/nested_route/dummy_models_controller.rb +26 -0
- data/spec/dummy/app/controllers/redirect/dummy_models_controller.rb +10 -0
- data/spec/dummy/app/controllers/redirect_auth/dummy_models_controller.rb +28 -0
- data/spec/dummy/app/controllers/scoped/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/controllers/scoped_params/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/controllers/strict/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/models/dummy_model.rb +4 -0
- data/spec/dummy/app/models/dummy_model_policy.rb +9 -1
- data/spec/dummy/app/views/block/dummy_models/index.html.erb +3 -0
- data/spec/dummy/app/views/block_new/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/block_show/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html/dummy_models/index.html.erb +3 -0
- data/spec/dummy/app/views/html_create/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_destroy/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_new/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_scoped/dummy_models/index.html.erb +3 -0
- data/spec/dummy/app/views/html_show/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_update/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/app/views/invalid_status/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/layouts/application.html.erb +4 -0
- data/spec/dummy/app/views/redirect/dummy_models/new.html.erb +1 -0
- data/spec/dummy/config/routes.rb +110 -0
- data/spec/dummy/db/migrate/20260806000000_add_slug_to_dummy_models.rb +7 -0
- data/spec/dummy/db/migrate/20260807000000_add_classroom_slug_to_dummy_models.rb +7 -0
- data/spec/dummy/db/schema.rb +3 -1
- data/spec/dummy/spec/factories/dummy_models.rb +1 -0
- data/spec/dummy_controller_spec.rb +1 -0
- data/spec/dummy_model_policy_spec.rb +6 -4
- data/spec/finder/dummy_models_controller_spec.rb +13 -0
- data/spec/html/dummy_models_controller_spec.rb +7 -0
- data/spec/html_create/dummy_models_controller_spec.rb +7 -0
- data/spec/html_destroy/dummy_models_controller_spec.rb +7 -0
- data/spec/html_finder/dummy_models_controller_spec.rb +11 -0
- data/spec/html_new/dummy_models_controller_spec.rb +7 -0
- data/spec/html_scoped/dummy_models_controller_spec.rb +7 -0
- data/spec/html_show/dummy_models_controller_spec.rb +7 -0
- data/spec/html_update/dummy_models_controller_spec.rb +7 -0
- data/spec/invalid_status/dummy_models_controller_spec.rb +10 -0
- data/spec/nested/dummy_models_controller_spec.rb +12 -0
- data/spec/nested_route/dummy_models_controller_spec.rb +19 -0
- data/spec/redirect/dummy_models_controller_spec.rb +23 -0
- data/spec/redirect_auth/dummy_models_controller_spec.rb +11 -0
- data/spec/scoped/dummy_models_controller_spec.rb +7 -0
- data/spec/scoped_params/dummy_models_controller_spec.rb +12 -0
- data/spec/simple_crud/authorization/action_policy_adapter_spec.rb +1 -9
- data/spec/simple_crud/authorization/can_can_can_adapter_spec.rb +1 -7
- data/spec/simple_crud/authorization/pundit_adapter_spec.rb +41 -0
- data/spec/simple_crud/controller_helpers_spec.rb +49 -0
- data/spec/simple_crud/pagination/adapters_spec.rb +37 -13
- data/spec/simple_crud/rspec_config_spec.rb +100 -0
- data/spec/simple_crud_configuration_spec.rb +19 -16
- data/spec/simple_crud_controller_spec.rb +4 -4
- data/spec/spec_helper.rb +1 -0
- data/spec/strict/dummy_models_controller_spec.rb +23 -0
- data/spec/unpaginated_scoped/dummy_models_controller_spec.rb +7 -0
- metadata +108 -4
- data/lib/spec/shared_examples/helpers.rb +0 -50
|
@@ -10,10 +10,31 @@ module SimpleCrud
|
|
|
10
10
|
class KaminariAdapter
|
|
11
11
|
include Adapter
|
|
12
12
|
|
|
13
|
-
def paginate(controller,
|
|
14
|
-
records =
|
|
13
|
+
def paginate(controller, relation, options)
|
|
14
|
+
records = paginated_records(controller, relation, options)
|
|
15
15
|
controller.render({ json: records }.merge(options))
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# Manual page scope: will_paginate shadows Relation#page when both load.
|
|
19
|
+
def paginated_records(controller, relation, _options)
|
|
20
|
+
page = [controller.params[:page].to_i, 1].max
|
|
21
|
+
per_page = page_size(controller)
|
|
22
|
+
|
|
23
|
+
relation.limit(per_page).offset(per_page * (page - 1)).extending do
|
|
24
|
+
include Kaminari::ActiveRecordRelationMethods
|
|
25
|
+
include Kaminari::PageScopeMethods
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Params arrive as strings: coerce before arithmetic or "5" * 2 == "55".
|
|
32
|
+
def page_size(controller)
|
|
33
|
+
size = (controller.params[:per_page] || Kaminari.config.default_per_page).to_i
|
|
34
|
+
size = [size, 1].max
|
|
35
|
+
size = [size, Kaminari.config.max_per_page].min if Kaminari.config.max_per_page
|
|
36
|
+
size
|
|
37
|
+
end
|
|
17
38
|
end
|
|
18
39
|
end
|
|
19
40
|
end
|
|
@@ -13,10 +13,15 @@ module SimpleCrud
|
|
|
13
13
|
# the pre-rename 43.4.x, so pick whichever key the loaded Pagy expects.
|
|
14
14
|
MAX_LIMIT_KEY = Gem::Version.new(Pagy::VERSION) >= Gem::Version.new('43.5.0') ? :max_limit : :client_max_limit
|
|
15
15
|
|
|
16
|
-
def paginate(controller,
|
|
17
|
-
|
|
16
|
+
def paginate(controller, relation, options)
|
|
17
|
+
records = paginated_records(controller, relation, options)
|
|
18
18
|
controller.render({ json: records }.merge(options))
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
def paginated_records(controller, relation, _options)
|
|
22
|
+
_pagy, records = controller.send(:pagy, relation, MAX_LIMIT_KEY => 100)
|
|
23
|
+
records
|
|
24
|
+
end
|
|
20
25
|
end
|
|
21
26
|
end
|
|
22
27
|
end
|
|
@@ -11,11 +11,15 @@ module SimpleCrud
|
|
|
11
11
|
class WillPaginateAdapter
|
|
12
12
|
include Adapter
|
|
13
13
|
|
|
14
|
-
def paginate(controller,
|
|
14
|
+
def paginate(controller, relation, options)
|
|
15
|
+
records = paginated_records(controller, relation, options)
|
|
16
|
+
controller.render({ json: records }.merge(options))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def paginated_records(controller, relation, _options)
|
|
15
20
|
page = controller.params[:page] || 1
|
|
16
21
|
per_page = controller.params[:per_page] || WillPaginate.per_page
|
|
17
|
-
|
|
18
|
-
controller.render({ json: records }.merge(options))
|
|
22
|
+
relation.paginate(page: page, per_page: per_page)
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -9,8 +9,13 @@ module SimpleCrud
|
|
|
9
9
|
class WorPaginateAdapter
|
|
10
10
|
include Adapter
|
|
11
11
|
|
|
12
|
-
def paginate(controller,
|
|
13
|
-
controller.send(:render_paginated,
|
|
12
|
+
def paginate(controller, relation, options)
|
|
13
|
+
controller.send(:render_paginated, relation, options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def paginated_records(controller, relation, options)
|
|
17
|
+
adapter = controller.send(:find_adapter_for_content, relation, options)
|
|
18
|
+
adapter.paginated_content
|
|
14
19
|
end
|
|
15
20
|
end
|
|
16
21
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
# Test helpers: SimpleCrud::RSpec.configure exposes the shared examples'
|
|
5
|
+
# configuration for apps on a different stack than the gem's.
|
|
6
|
+
module RSpec
|
|
7
|
+
def self.configure
|
|
8
|
+
yield Config.instance
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Configures how the shared examples create records, authenticate
|
|
12
|
+
# requests and introspect policies/serializers. The defaults match the
|
|
13
|
+
# gem's own stack (Devise-JWT + FactoryBot + Pundit + ActiveModel
|
|
14
|
+
# Serializers), so apps on a different stack can override them via
|
|
15
|
+
# SimpleCrud::RSpec.configure instead of editing the examples.
|
|
16
|
+
#
|
|
17
|
+
# Callable settings (authenticate, current_user, other_user,
|
|
18
|
+
# create_record, create_records, params_for, model_attributes,
|
|
19
|
+
# policy_class, serializer_class) run in the example-group context, so
|
|
20
|
+
# they can call helpers such as `create`, `request` and `current_user`.
|
|
21
|
+
class Config
|
|
22
|
+
DEFAULTS = {
|
|
23
|
+
authenticate: lambda {
|
|
24
|
+
headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
|
|
25
|
+
request.headers.merge!(Devise::JWT::TestHelpers.auth_headers(headers, current_user))
|
|
26
|
+
},
|
|
27
|
+
current_user: -> { create(:user) },
|
|
28
|
+
other_user: -> { create(:user) },
|
|
29
|
+
create_record: ->(klass, attributes) { create(klass, **attributes) },
|
|
30
|
+
create_records: ->(klass, count, attributes) { create_list(klass, count, **attributes) },
|
|
31
|
+
params_for: ->(klass) { attributes_for(klass) },
|
|
32
|
+
owner_association: :user,
|
|
33
|
+
model_attributes: -> { owner_association ? { owner_association => current_user } : {} },
|
|
34
|
+
scoped_attributes: -> { model_attributes },
|
|
35
|
+
other_scoped_attributes: lambda {
|
|
36
|
+
attributes = model_attributes
|
|
37
|
+
if owner_association && !attributes.is_a?(Hash)
|
|
38
|
+
raise ArgumentError,
|
|
39
|
+
'model_attributes must resolve to a Hash when owner_association is set; ' \
|
|
40
|
+
'override other_scoped_attributes instead'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
owner_association ? attributes.merge(owner_association => other_user) : {}
|
|
44
|
+
},
|
|
45
|
+
required_attribute: :name,
|
|
46
|
+
required_error: "Name can't be blank",
|
|
47
|
+
finder_key: :slug,
|
|
48
|
+
params_key: nil,
|
|
49
|
+
route_params: -> { {} },
|
|
50
|
+
invalid_status: :ok,
|
|
51
|
+
unauthenticated_status: :unauthorized,
|
|
52
|
+
assert_html_template: true,
|
|
53
|
+
policy_class: ->(klass) { "#{klass}Policy".constantize },
|
|
54
|
+
serializer_class: ->(model) { "#{model.class}_serializer".classify.constantize },
|
|
55
|
+
created_record_check: nil
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
def instance
|
|
60
|
+
@instance ||= new
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
@values = {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
DEFAULTS.each_key do |key|
|
|
69
|
+
define_method(key) { @values.fetch(key, DEFAULTS[key]) }
|
|
70
|
+
define_method("#{key}=") { |value| @values[key] = value }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
module RSpec
|
|
5
|
+
module Helpers
|
|
6
|
+
# Current/other users, signing requests in and the owner association
|
|
7
|
+
# attributed to the current user.
|
|
8
|
+
module Authentication
|
|
9
|
+
def current_user
|
|
10
|
+
@current_user ||= instance_exec(&setting(:current_user))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def other_user
|
|
14
|
+
@other_user ||= instance_exec(&setting(:other_user))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def authenticate_request
|
|
18
|
+
instance_exec(&setting(:authenticate))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def owner_foreign_key
|
|
22
|
+
:"#{owner_association}_id"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The owner association attributed to current_user, if the model has one.
|
|
26
|
+
def owner_params
|
|
27
|
+
owner_association ? { owner_foreign_key => current_user.id } : {}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
module RSpec
|
|
5
|
+
module Helpers
|
|
6
|
+
# Definition-time introspection of the options each action was declared
|
|
7
|
+
# with via simple_crud_for (read from the controller's metadata).
|
|
8
|
+
module ControllerOptions
|
|
9
|
+
def get_option(method, option)
|
|
10
|
+
described_class.instance_variable_get(:@simple_crud_metadata)[method][option]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
%i[paginate authorize authenticate serializer html finder scope build raise_on_invalid block].each do |option|
|
|
14
|
+
define_method("check_#{option}") { |method| get_option(method, option) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
module RSpec
|
|
5
|
+
module Helpers
|
|
6
|
+
# Building the record under test and the params used to persist it.
|
|
7
|
+
module Models
|
|
8
|
+
def model_class
|
|
9
|
+
described_class.to_s.split('::')
|
|
10
|
+
.last.sub('Controller', '').singularize.underscore
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def model_class_object
|
|
14
|
+
model_class.classify.constantize
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def model
|
|
18
|
+
@model ||= create_record(model_class, model_attributes)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def model_attributes
|
|
22
|
+
resolve(setting(:model_attributes))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_record(klass, attributes)
|
|
26
|
+
instance_exec(klass, attributes, &setting(:create_record))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_records(klass, count, attributes)
|
|
30
|
+
instance_exec(klass, count, attributes, &setting(:create_records))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def params_for(klass)
|
|
34
|
+
instance_exec(klass, &setting(:params_for))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def model_params
|
|
38
|
+
@model_params ||= params_for(model_class)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def model_serializer
|
|
42
|
+
defined?(serializer) ? serializer : instance_exec(model, &setting(:serializer_class))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rendered_record
|
|
46
|
+
controller.instance_variable_get(:@record)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def rendered_records
|
|
50
|
+
controller.instance_variable_get(:@records)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
module RSpec
|
|
5
|
+
module Helpers
|
|
6
|
+
# Locating the policy object for the model under test and stubbing it
|
|
7
|
+
# to allow or deny.
|
|
8
|
+
module Policies
|
|
9
|
+
def policy_class_object
|
|
10
|
+
instance_exec(model_class_object, &setting(:policy_class))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def make_policies_fail(method)
|
|
14
|
+
allow(policy_class_object).to receive(:new)
|
|
15
|
+
.and_return(instance_double(policy_class_object, "#{method}?" => false))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def make_policies_succeed(method)
|
|
19
|
+
allow(policy_class_object).to receive(:new)
|
|
20
|
+
.and_return(instance_double(policy_class_object, "#{method}?" => true))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
module RSpec
|
|
5
|
+
module Helpers
|
|
6
|
+
# Request params and format helpers shared by every action example.
|
|
7
|
+
module Requests
|
|
8
|
+
# :unprocessable_entity was renamed :unprocessable_content in Rack 3.1.
|
|
9
|
+
# Resolve the current Rack's canonical symbol for 422.
|
|
10
|
+
def unprocessable_status
|
|
11
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE.invert[422]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def request_format(action)
|
|
15
|
+
check_html(action) ? :html : :json
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Extra params (e.g. a parent slug) added to every request, for nested
|
|
19
|
+
# resources like /projects/:project_slug/tasks.
|
|
20
|
+
def route_params
|
|
21
|
+
resolve(setting(:route_params))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def with_route_params(attrs)
|
|
25
|
+
route_params.merge(attrs)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The params that identify a record for the given action, using the
|
|
29
|
+
# configured finder_key when the action has a custom finder.
|
|
30
|
+
def record_param(action, record, not_found: false)
|
|
31
|
+
key = check_finder(action) ? finder_key : :id
|
|
32
|
+
value = not_found ? "nonexistent-#{key}" : record.public_send(key)
|
|
33
|
+
{ key => value }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Wraps a request body under the model's strong-params key when
|
|
37
|
+
# params_key is configured (nested strong params), else passes it flat.
|
|
38
|
+
def body_params(attrs)
|
|
39
|
+
key = params_key
|
|
40
|
+
key ? { key => attrs } : attrs
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
module RSpec
|
|
5
|
+
module Helpers
|
|
6
|
+
# Settings resolution: `simple_crud:` example-group metadata first,
|
|
7
|
+
# falling back to the app-wide SimpleCrud::RSpec::Config singleton.
|
|
8
|
+
module Settings
|
|
9
|
+
def config
|
|
10
|
+
SimpleCrud::RSpec::Config.instance
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def simple_crud_settings
|
|
14
|
+
source = ::RSpec.current_example ? ::RSpec.current_example.metadata : metadata
|
|
15
|
+
source[:simple_crud] || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setting(name)
|
|
19
|
+
simple_crud_settings.fetch(name) { config.public_send(name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolve(value)
|
|
23
|
+
value.is_a?(Proc) ? instance_exec(&value) : value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
%i[owner_association required_attribute required_error finder_key params_key invalid_status
|
|
27
|
+
unauthenticated_status assert_html_template scoped_attributes other_scoped_attributes].each do |name|
|
|
28
|
+
define_method(name) { resolve(setting(name)) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def created_record_check
|
|
32
|
+
setting(:created_record_check)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Runs the block with a config setting temporarily overridden, restoring
|
|
36
|
+
# the original value afterwards (even if the block raises). Useful when
|
|
37
|
+
# a controller's spec needs a different setting than the app-wide default.
|
|
38
|
+
def with_config_override(setting, value)
|
|
39
|
+
config_instance = SimpleCrud::RSpec::Config.instance
|
|
40
|
+
original = config_instance.public_send(setting)
|
|
41
|
+
SimpleCrud::RSpec.configure { |c| c.public_send("#{setting}=", value) }
|
|
42
|
+
yield
|
|
43
|
+
ensure
|
|
44
|
+
SimpleCrud::RSpec.configure { |c| c.public_send("#{setting}=", original) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helpers/settings'
|
|
4
|
+
require_relative 'helpers/controller_options'
|
|
5
|
+
require_relative 'helpers/models'
|
|
6
|
+
require_relative 'helpers/authentication'
|
|
7
|
+
require_relative 'helpers/requests'
|
|
8
|
+
require_relative 'helpers/policies'
|
|
9
|
+
|
|
10
|
+
module SimpleCrud
|
|
11
|
+
module RSpec
|
|
12
|
+
# Helpers available to the shared examples (and to apps that include
|
|
13
|
+
# them), composed from focused sub-modules: settings resolution,
|
|
14
|
+
# controller-option introspection, record building, authentication,
|
|
15
|
+
# request params and policy access.
|
|
16
|
+
module Helpers
|
|
17
|
+
include Settings
|
|
18
|
+
include ControllerOptions
|
|
19
|
+
include Models
|
|
20
|
+
include Authentication
|
|
21
|
+
include Requests
|
|
22
|
+
include Policies
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/simple_crud/rspec.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'rspec/config'
|
|
4
|
+
require_relative 'rspec/helpers'
|
|
3
5
|
require_relative '../spec/response_helper'
|
|
4
6
|
require_relative '../spec/shared_contexts/authenticate_user'
|
|
5
7
|
require_relative '../spec/shared_examples/simple_crud_for_update'
|
|
@@ -7,5 +9,38 @@ require_relative '../spec/shared_examples/simple_crud_for_show'
|
|
|
7
9
|
require_relative '../spec/shared_examples/simple_crud_for_index'
|
|
8
10
|
require_relative '../spec/shared_examples/simple_crud_for_destroy'
|
|
9
11
|
require_relative '../spec/shared_examples/simple_crud_for_create'
|
|
12
|
+
require_relative '../spec/shared_examples/simple_crud_for_new'
|
|
13
|
+
require_relative '../spec/shared_examples/simple_crud_for_edit'
|
|
10
14
|
require_relative '../spec/shared_examples/unauthorized_when_not_logged_in'
|
|
15
|
+
require_relative '../spec/shared_examples/simple_crud_renders_template'
|
|
16
|
+
require_relative '../spec/shared_examples/simple_crud_without_authenticated_user'
|
|
17
|
+
require_relative '../spec/shared_examples/simple_crud_when_not_authorized'
|
|
18
|
+
require_relative '../spec/shared_examples/simple_crud_for_index_with_block'
|
|
19
|
+
require_relative '../spec/shared_examples/simple_crud_for_index_with_scope'
|
|
20
|
+
require_relative '../spec/shared_examples/simple_crud_for_show_with_block'
|
|
21
|
+
require_relative '../spec/shared_examples/simple_crud_for_new_with_block'
|
|
22
|
+
require_relative '../spec/shared_examples/simple_crud_for_create_with_block'
|
|
23
|
+
require_relative '../spec/shared_examples/simple_crud_for_destroy_with_block'
|
|
24
|
+
require_relative '../spec/shared_examples/simple_crud_for_show_with_finder'
|
|
25
|
+
require_relative '../spec/shared_examples/simple_crud_for_update_with_finder'
|
|
26
|
+
require_relative '../spec/shared_examples/simple_crud_for_destroy_with_finder'
|
|
27
|
+
require_relative '../spec/shared_examples/simple_crud_not_found_with_finder'
|
|
28
|
+
require_relative '../spec/shared_examples/simple_crud_for_new_with_build'
|
|
29
|
+
require_relative '../spec/shared_examples/simple_crud_for_create_with_build'
|
|
30
|
+
require_relative '../spec/shared_examples/authorization_adapter_authorize'
|
|
11
31
|
require_relative '../spec/matchers/have_been_serialized_with'
|
|
32
|
+
|
|
33
|
+
module SimpleCrud
|
|
34
|
+
# Opt-in wiring for the shared examples: see .install!
|
|
35
|
+
module RSpec
|
|
36
|
+
# Opt-in wiring: call from your spec support after requiring this file.
|
|
37
|
+
# extend wires definition-time helpers (check_html...), include runtime ones (model...).
|
|
38
|
+
def self.install!
|
|
39
|
+
::RSpec.configure do |config|
|
|
40
|
+
config.extend Helpers
|
|
41
|
+
config.include Helpers
|
|
42
|
+
config.include Response::JSONParser, type: :controller
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_support/all'
|
|
4
4
|
require_relative 'config'
|
|
5
|
+
require_relative 'controller_helpers'
|
|
6
|
+
require_relative 'action_lambdas'
|
|
5
7
|
|
|
6
8
|
# Extended onto a controller for CRUD actions.
|
|
7
9
|
module SimpleCrudController
|
|
10
|
+
extend SimpleCrud::ControllerHelpers
|
|
11
|
+
# Include, not extend: controllers gain these by extending SimpleCrudController.
|
|
12
|
+
include SimpleCrud::ActionLambdas
|
|
13
|
+
|
|
8
14
|
cattr_accessor :params, :permitted
|
|
9
15
|
|
|
10
16
|
# Possible options:
|
|
@@ -12,21 +18,31 @@ module SimpleCrudController
|
|
|
12
18
|
### paginate: paginate the list via Config.pagination_adapter (wor-paginate by default)
|
|
13
19
|
### authenticate: use devise to authenticate
|
|
14
20
|
### serializer: use a particular serializer (both each_serializer and serializer)
|
|
15
|
-
|
|
21
|
+
### html: render the action's ERB template instead of JSON (index/show/new/edit/create/update/destroy)
|
|
22
|
+
### finder: custom record lookup (Proc/lambda or Symbol) for :show/:update/:destroy/:edit
|
|
23
|
+
### scope: custom index scope (Proc/lambda taking current_user and optional params), overrides policy_scope
|
|
24
|
+
### build: custom record builder (Proc/lambda) for :new/:create, invoked with the controller as self
|
|
25
|
+
### raise_on_invalid: use strict create!/update! semantics instead of returning 422
|
|
26
|
+
### A block given to simple_crud_for renders explicitly: it receives the records for :index,
|
|
27
|
+
### the record for :show/:new/:edit, or the record and a saved flag for :create/:update/:destroy
|
|
28
|
+
def simple_crud_for(method, parameters = {}, &block)
|
|
29
|
+
parameters[:html] = true if block && !parameters.key?(:html)
|
|
30
|
+
parameters[:block] = true if block
|
|
16
31
|
parameters = parameters_with_defaults(parameters)
|
|
17
32
|
klass = simple_crud_controller_model
|
|
18
33
|
check_valid_method(method)
|
|
19
34
|
check_policies(parameters)
|
|
20
35
|
check_serializer(parameters)
|
|
21
|
-
define_method(method, send("crud_lambda_for_#{method}", klass, parameters))
|
|
36
|
+
define_method(method, send("crud_lambda_for_#{method}", klass, parameters, &block))
|
|
22
37
|
write_metadata(method, parameters)
|
|
23
38
|
end
|
|
24
39
|
|
|
25
40
|
def parameters_with_defaults(parameters)
|
|
26
|
-
defaults = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
defaults = {
|
|
42
|
+
authorize: true, paginate: true, authenticate: true, serializer: nil,
|
|
43
|
+
html: false, finder: nil, scope: nil, build: nil, raise_on_invalid: false
|
|
44
|
+
}
|
|
45
|
+
defaults.each { |key, value| parameters[key] = value unless parameters.key?(key) }
|
|
30
46
|
parameters
|
|
31
47
|
end
|
|
32
48
|
|
|
@@ -35,74 +51,12 @@ module SimpleCrudController
|
|
|
35
51
|
@simple_crud_metadata[method] = parameters
|
|
36
52
|
end
|
|
37
53
|
|
|
38
|
-
def self.maybe_authorize(controller, record, parameters)
|
|
39
|
-
return unless parameters[:authorize] && parameters[:authenticate]
|
|
40
|
-
|
|
41
|
-
SimpleCrud::Config.authorization_adapter.authorize(controller, record)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def crud_lambda_for_show(klass, parameters = {})
|
|
45
|
-
lambda do
|
|
46
|
-
authenticate_user! if parameters[:authenticate]
|
|
47
|
-
requested = klass.find(params[:id])
|
|
48
|
-
|
|
49
|
-
options = {}.merge(serializer: parameters[:serializer]).compact
|
|
50
|
-
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
51
|
-
render({ json: requested }.merge(options))
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def crud_lambda_for_index(klass, parameters = {})
|
|
56
|
-
lambda do
|
|
57
|
-
authenticate_user! if parameters[:authenticate]
|
|
58
|
-
SimpleCrudController.maybe_authorize(self, klass.new, parameters)
|
|
59
|
-
options = {}.merge(each_serializer: parameters[:serializer]).compact
|
|
60
|
-
SimpleCrudController.render_index(self, klass, options, parameters)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def self.render_index(controller, klass, options, parameters)
|
|
65
|
-
if parameters[:paginate]
|
|
66
|
-
SimpleCrud::Config.pagination_adapter.paginate(controller, klass, options)
|
|
67
|
-
else
|
|
68
|
-
controller.render({ json: klass.all }.merge(options))
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def crud_lambda_for_create(klass, parameters = {})
|
|
73
|
-
lambda do
|
|
74
|
-
authenticate_user! if parameters[:authenticate]
|
|
75
|
-
permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
|
|
76
|
-
SimpleCrudController.maybe_authorize(self, klass.new(permitted_params), parameters)
|
|
77
|
-
render json: klass.create!(permitted_params), status: :created
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def crud_lambda_for_update(klass, parameters = {})
|
|
82
|
-
lambda do
|
|
83
|
-
authenticate_user! if parameters[:authenticate]
|
|
84
|
-
requested = klass.find(params[:id])
|
|
85
|
-
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
86
|
-
permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
|
|
87
|
-
render json: requested.update!(permitted_params)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def crud_lambda_for_destroy(klass, parameters = {})
|
|
92
|
-
lambda do
|
|
93
|
-
authenticate_user! if parameters[:authenticate]
|
|
94
|
-
requested = klass.find(params[:id])
|
|
95
|
-
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
96
|
-
render json: klass.find(params[:id]).destroy
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
54
|
def simple_crud_controller_model
|
|
101
55
|
to_s.split('::').last.sub('Controller', '').singularize.classify.constantize
|
|
102
56
|
end
|
|
103
57
|
|
|
104
58
|
def check_valid_method(method)
|
|
105
|
-
|
|
59
|
+
raise ArgumentError, 'invalid method' unless %i[show index create update destroy new edit].include? method
|
|
106
60
|
end
|
|
107
61
|
|
|
108
62
|
def check_policies(parameters)
|
|
@@ -111,15 +65,13 @@ module SimpleCrudController
|
|
|
111
65
|
model = simple_crud_controller_model
|
|
112
66
|
return if SimpleCrud::Config.authorization_adapter.policy_defined?(model)
|
|
113
67
|
|
|
114
|
-
|
|
68
|
+
raise ArgumentError, "no authorization policy configured for #{model}"
|
|
115
69
|
end
|
|
116
70
|
|
|
117
71
|
def check_serializer(parameters)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
serializer_name = parameters[:serializer].to_s
|
|
121
|
-
return if Kernel.const_defined?(serializer_name)
|
|
72
|
+
name = parameters[:serializer].to_s
|
|
73
|
+
return if name.blank? || Kernel.const_defined?(name)
|
|
122
74
|
|
|
123
|
-
|
|
75
|
+
raise ArgumentError, "create a valid serializer with name #{name}"
|
|
124
76
|
end
|
|
125
77
|
end
|
data/lib/simple_crud/version.rb
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
RSpec.shared_context 'with authenticated user' do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
before do
|
|
7
|
-
headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
|
|
8
|
-
auth_headers = Devise::JWT::TestHelpers.auth_headers(headers, current_user)
|
|
9
|
-
request.headers.merge!(auth_headers)
|
|
10
|
-
end
|
|
4
|
+
before { authenticate_request }
|
|
11
5
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
shared_examples 'authorization adapter #authorize' do |record_class, denied_error|
|
|
4
|
+
it 'passes for a record the adapter allows' do
|
|
5
|
+
expect { adapter.authorize(controller, record_class.new(1)) }.not_to raise_error
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'raises for a record the adapter denies' do
|
|
9
|
+
expect { adapter.authorize(controller, record_class.new(2)) }.to raise_error(denied_error)
|
|
10
|
+
end
|
|
11
|
+
end
|