servactory 2.16.1 → 3.0.0.rc2
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/README.md +38 -9
- data/config/locales/de.yml +134 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +134 -0
- data/config/locales/fr.yml +134 -0
- data/config/locales/it.yml +134 -0
- data/config/locales/ru.yml +15 -0
- data/lib/generators/README.md +45 -0
- data/lib/generators/servactory/base.rb +82 -0
- data/lib/generators/servactory/extension/USAGE +54 -0
- data/lib/generators/servactory/extension/extension_generator.rb +41 -0
- data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
- data/lib/generators/servactory/install/USAGE +27 -0
- data/lib/generators/servactory/install/install_generator.rb +94 -0
- data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
- data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
- data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
- data/lib/generators/servactory/rspec/USAGE +46 -0
- data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
- data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
- data/lib/generators/servactory/service/USAGE +51 -0
- data/lib/generators/servactory/service/service_generator.rb +56 -0
- data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
- data/lib/servactory/actions/collection.rb +56 -1
- data/lib/servactory/actions/dsl.rb +11 -11
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +111 -28
- data/lib/servactory/base.rb +1 -7
- data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
- data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
- data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/config.rb +36 -0
- data/lib/servactory/configuration/configurable.rb +95 -0
- data/lib/servactory/configuration/dsl.rb +3 -27
- data/lib/servactory/configuration/factory.rb +20 -20
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
- data/lib/servactory/context/warehouse/inputs.rb +2 -2
- data/lib/servactory/context/workspace/inputs.rb +2 -2
- data/lib/servactory/context/workspace/internals.rb +2 -2
- data/lib/servactory/context/workspace/outputs.rb +2 -2
- data/lib/servactory/context/workspace.rb +11 -7
- data/lib/servactory/dsl.rb +10 -8
- data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
- data/lib/servactory/result.rb +2 -2
- data/lib/servactory/test_kit/fake_type.rb +23 -0
- data/lib/servactory/test_kit/result.rb +45 -0
- data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
- data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
- data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
- data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
- data/lib/servactory/test_kit/utils/faker.rb +62 -2
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
- data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
- data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
- data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
- data/lib/servactory/tool_kit/dynamic_options/target.rb +248 -0
- data/lib/servactory/version.rb +4 -4
- data/lib/servactory.rb +6 -0
- metadata +57 -48
- data/lib/generators/servactory/install_generator.rb +0 -21
- data/lib/generators/servactory/rspec_generator.rb +0 -88
- data/lib/generators/servactory/service_generator.rb +0 -49
- data/lib/servactory/configuration/setup.rb +0 -97
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Input
|
|
9
|
+
# Submatcher for validating input default values.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that a service input has the expected default value.
|
|
14
|
+
# Useful for testing optional inputs with predefined fallback values.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# it { is_expected.to have_service_input(:limit).default(10) }
|
|
20
|
+
# it { is_expected.to have_service_input(:enabled).default(true) }
|
|
21
|
+
# it { is_expected.to have_service_input(:options).default({}) }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Comparison
|
|
25
|
+
#
|
|
26
|
+
# Uses case-insensitive string comparison for value matching.
|
|
27
|
+
# Handles nil values specially - matches only when expected is also nil.
|
|
28
|
+
class DefaultSubmatcher < Base::Submatcher
|
|
29
|
+
# Creates a new default submatcher.
|
|
30
|
+
#
|
|
31
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
32
|
+
# @param expected_value [Object] The expected default value
|
|
33
|
+
# @return [DefaultSubmatcher] New submatcher instance
|
|
34
|
+
def initialize(context, expected_value)
|
|
35
|
+
super(context)
|
|
36
|
+
@expected_value = expected_value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns description for RSpec output.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] Human-readable description with expected value
|
|
42
|
+
def description
|
|
43
|
+
"default: #{expected_value.inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
# Checks if the input default matches the expected value.
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] True if default values match
|
|
51
|
+
def passes?
|
|
52
|
+
actual_default = attribute_data.fetch(:default)
|
|
53
|
+
@actual_value = actual_default
|
|
54
|
+
|
|
55
|
+
return expected_value.nil? if actual_default.is_a?(NilClass)
|
|
56
|
+
|
|
57
|
+
actual_default.to_s.casecmp(expected_value.to_s).zero?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Builds the failure message for default value validation.
|
|
61
|
+
#
|
|
62
|
+
# @return [String] Failure message with expected vs actual
|
|
63
|
+
def build_failure_message
|
|
64
|
+
<<~MESSAGE
|
|
65
|
+
should have a default value
|
|
66
|
+
|
|
67
|
+
expected #{expected_value.inspect}
|
|
68
|
+
got #{@actual_value.inspect}
|
|
69
|
+
MESSAGE
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :expected_value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Input
|
|
9
|
+
# Submatcher for validating that an input is optional.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that a service input has `required: false` option set,
|
|
14
|
+
# meaning the input can be omitted when calling the service.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# it { is_expected.to have_service_input(:nickname).optional }
|
|
20
|
+
# it { is_expected.to have_service_input(:description).optional }
|
|
21
|
+
# ```
|
|
22
|
+
#
|
|
23
|
+
# ## Note
|
|
24
|
+
#
|
|
25
|
+
# Mutually exclusive with `required` submatcher - only one can be used
|
|
26
|
+
# per matcher chain.
|
|
27
|
+
class OptionalSubmatcher < Base::Submatcher
|
|
28
|
+
# Returns description for RSpec output.
|
|
29
|
+
#
|
|
30
|
+
# @return [String] Human-readable description
|
|
31
|
+
def description
|
|
32
|
+
"required: false"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
# Checks if the input is optional (required: false).
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean] True if input is not required
|
|
40
|
+
def passes?
|
|
41
|
+
input_required = attribute_data.fetch(:required).fetch(:is)
|
|
42
|
+
input_required == false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Builds the failure message for optional validation.
|
|
46
|
+
#
|
|
47
|
+
# @return [String] Failure message with expected vs actual
|
|
48
|
+
def build_failure_message
|
|
49
|
+
<<~MESSAGE
|
|
50
|
+
should be optional
|
|
51
|
+
|
|
52
|
+
expected required: false
|
|
53
|
+
got required: true
|
|
54
|
+
MESSAGE
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Input
|
|
9
|
+
# Submatcher for validating that an input is required.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that a service input has `required: true` option set.
|
|
14
|
+
# Optionally validates a custom error message for the required validation.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# it { is_expected.to have_service_input(:user_id).required }
|
|
20
|
+
# it { is_expected.to have_service_input(:name).required("Name is mandatory") }
|
|
21
|
+
# ```
|
|
22
|
+
#
|
|
23
|
+
# ## Validation
|
|
24
|
+
#
|
|
25
|
+
# Checks the `:required` option in attribute data where `is: true`.
|
|
26
|
+
# If a custom message is provided, also validates the message matches.
|
|
27
|
+
class RequiredSubmatcher < Base::Submatcher
|
|
28
|
+
# Creates a new required submatcher.
|
|
29
|
+
#
|
|
30
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
31
|
+
# @param custom_message [String, nil] Optional expected error message
|
|
32
|
+
# @return [RequiredSubmatcher] New submatcher instance
|
|
33
|
+
def initialize(context, custom_message = nil)
|
|
34
|
+
super(context)
|
|
35
|
+
@custom_message = custom_message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns description for RSpec output.
|
|
39
|
+
#
|
|
40
|
+
# @return [String] Human-readable description
|
|
41
|
+
def description
|
|
42
|
+
"required: true"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
# Checks if the input is required and message matches if specified.
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean] True if input is required with matching message
|
|
50
|
+
def passes?
|
|
51
|
+
required_data = attribute_data.fetch(:required)
|
|
52
|
+
is_required = required_data.fetch(:is)
|
|
53
|
+
|
|
54
|
+
return false unless is_required == true
|
|
55
|
+
return true unless custom_message.present?
|
|
56
|
+
|
|
57
|
+
actual_message = required_data.fetch(:message) || default_required_message
|
|
58
|
+
actual_message.casecmp(custom_message).zero?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Builds the failure message for required validation.
|
|
62
|
+
#
|
|
63
|
+
# @return [String] Failure message with expected vs actual
|
|
64
|
+
def build_failure_message
|
|
65
|
+
<<~MESSAGE
|
|
66
|
+
should be required
|
|
67
|
+
|
|
68
|
+
expected required: true
|
|
69
|
+
got required: false
|
|
70
|
+
MESSAGE
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :custom_message
|
|
76
|
+
|
|
77
|
+
# Generates the default I18n message for required validation.
|
|
78
|
+
#
|
|
79
|
+
# @return [String] Localized default required message
|
|
80
|
+
def default_required_message
|
|
81
|
+
I18n.t(
|
|
82
|
+
"#{i18n_root_key}.inputs.validations.required.default_error.default",
|
|
83
|
+
service_class_name: described_class.name,
|
|
84
|
+
input_name: attribute_name
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Input
|
|
9
|
+
# Submatcher for integration testing of input validation.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Performs integration testing of input attributes by calling the
|
|
14
|
+
# actual service and verifying validation behavior. Tests type checks,
|
|
15
|
+
# required/optional status, inclusion, and target validations.
|
|
16
|
+
#
|
|
17
|
+
# ## Usage
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# it { is_expected.to have_service_input(:status).valid_with(valid_attributes) }
|
|
21
|
+
# it { is_expected.to have_service_input(:count).valid_with(false) }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Deprecation Notice
|
|
25
|
+
#
|
|
26
|
+
# This submatcher is planned to be decommissioned. Consider using
|
|
27
|
+
# direct service call tests instead.
|
|
28
|
+
#
|
|
29
|
+
# ## Validation Steps
|
|
30
|
+
#
|
|
31
|
+
# 1. `success_passes?` - service succeeds with valid attributes
|
|
32
|
+
# 2. `failure_type_passes?` - fails correctly with wrong type
|
|
33
|
+
# 3. `failure_required_passes?` - fails when required input is nil
|
|
34
|
+
# 4. `failure_optional_passes?` - succeeds when optional input is nil
|
|
35
|
+
# 5. `failure_inclusion_passes?` - fails with value outside inclusion
|
|
36
|
+
# 6. `failure_target_passes?` - fails with value outside target
|
|
37
|
+
class ValidWithSubmatcher < Base::Submatcher # rubocop:disable Metrics/ClassLength
|
|
38
|
+
# Creates a new valid_with submatcher.
|
|
39
|
+
#
|
|
40
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
41
|
+
# @param attributes [Hash, FalseClass] Test attributes or false to skip
|
|
42
|
+
# @return [ValidWithSubmatcher] New submatcher instance
|
|
43
|
+
def initialize(context, attributes)
|
|
44
|
+
super(context)
|
|
45
|
+
@attributes = attributes.is_a?(FalseClass) ? attributes : Servactory::Utils.adapt(attributes)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns description for RSpec output.
|
|
49
|
+
#
|
|
50
|
+
# @return [String] Human-readable description
|
|
51
|
+
def description
|
|
52
|
+
"valid_with attribute checking"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
# Runs all validation checks in sequence.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] True if all validation scenarios pass
|
|
60
|
+
def passes?
|
|
61
|
+
return true if attributes.is_a?(FalseClass)
|
|
62
|
+
|
|
63
|
+
success_passes? &&
|
|
64
|
+
failure_type_passes? &&
|
|
65
|
+
failure_required_passes? &&
|
|
66
|
+
failure_optional_passes? &&
|
|
67
|
+
failure_inclusion_passes? &&
|
|
68
|
+
failure_target_passes?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Builds the failure message for valid_with validation.
|
|
72
|
+
#
|
|
73
|
+
# @return [String] Generic failure message
|
|
74
|
+
def build_failure_message
|
|
75
|
+
"should work as expected on the specified attributes based on its options"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
attr_reader :attributes
|
|
81
|
+
|
|
82
|
+
# Checks that service succeeds with valid attributes.
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] True if service call succeeds
|
|
85
|
+
def success_passes?
|
|
86
|
+
described_class.call!(attributes).success?
|
|
87
|
+
rescue Servactory::Exceptions::Input
|
|
88
|
+
false
|
|
89
|
+
rescue StandardError
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Checks that service fails with wrong type.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] True if type validation fails as expected
|
|
96
|
+
def failure_type_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
97
|
+
option_types = attribute_data.fetch(:types)
|
|
98
|
+
|
|
99
|
+
prepared_attributes = attributes.dup
|
|
100
|
+
prepared_attributes[attribute_name] = Servactory::TestKit::FakeType.new
|
|
101
|
+
|
|
102
|
+
input_required_message = I18n.t(
|
|
103
|
+
"#{i18n_root_key}.inputs.validations.type.default_error.default",
|
|
104
|
+
service_class_name: described_class.name,
|
|
105
|
+
input_name: attribute_name,
|
|
106
|
+
expected_type: option_types.join(", "),
|
|
107
|
+
given_type: Servactory::TestKit::FakeType.new.class.name
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
expect_failure_with!(prepared_attributes, input_required_message)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Checks that required validation fails when input is nil.
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] True if required validation fails as expected
|
|
116
|
+
def failure_required_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
117
|
+
input_required = attribute_data.fetch(:required).fetch(:is)
|
|
118
|
+
return true unless input_required
|
|
119
|
+
|
|
120
|
+
prepared_attributes = attributes.dup
|
|
121
|
+
prepared_attributes[attribute_name] = nil
|
|
122
|
+
|
|
123
|
+
input_required_message = attribute_data.fetch(:required).fetch(:message)
|
|
124
|
+
|
|
125
|
+
if input_required_message.nil?
|
|
126
|
+
input_required_message = I18n.t(
|
|
127
|
+
"#{i18n_root_key}.inputs.validations.required.default_error.default",
|
|
128
|
+
service_class_name: described_class.name,
|
|
129
|
+
input_name: attribute_name
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
expect_failure_with!(prepared_attributes, input_required_message)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Checks that optional input accepts nil without failure.
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean] True if service doesn't fail on nil optional
|
|
139
|
+
def failure_optional_passes?
|
|
140
|
+
input_required = attribute_data.fetch(:required).fetch(:is)
|
|
141
|
+
return true if input_required
|
|
142
|
+
|
|
143
|
+
prepared_attributes = attributes.dup
|
|
144
|
+
prepared_attributes[attribute_name] = nil
|
|
145
|
+
|
|
146
|
+
expect_failure_with!(prepared_attributes, nil)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Checks that inclusion validation fails with wrong value.
|
|
150
|
+
#
|
|
151
|
+
# @return [Boolean] True if inclusion validation fails as expected
|
|
152
|
+
def failure_inclusion_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
153
|
+
input_inclusion_in = attribute_data.dig(:inclusion, :in)
|
|
154
|
+
return true if input_inclusion_in.blank?
|
|
155
|
+
|
|
156
|
+
wrong_value = generate_wrong_value_for_inclusion(input_inclusion_in)
|
|
157
|
+
|
|
158
|
+
prepared_attributes = attributes.dup
|
|
159
|
+
prepared_attributes[attribute_name] = wrong_value
|
|
160
|
+
|
|
161
|
+
input_required_message = attribute_data.fetch(:inclusion).fetch(:message)
|
|
162
|
+
|
|
163
|
+
# If message is a Proc, we can't easily evaluate it in the test context
|
|
164
|
+
# (it may require runtime args like input:, value:), so skip message comparison
|
|
165
|
+
if input_required_message.is_a?(Proc)
|
|
166
|
+
return expect_failure_with!(prepared_attributes, :skip_message_check)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if input_required_message.nil?
|
|
170
|
+
input_required_message = I18n.t(
|
|
171
|
+
"#{i18n_root_key}.inputs.validations.must.dynamic_options.inclusion.default",
|
|
172
|
+
service_class_name: described_class.name,
|
|
173
|
+
input_name: attribute_name,
|
|
174
|
+
input_inclusion: input_inclusion_in.inspect,
|
|
175
|
+
value: wrong_value.inspect
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
expect_failure_with!(prepared_attributes, input_required_message)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Checks that target validation fails with wrong value.
|
|
183
|
+
#
|
|
184
|
+
# @return [Boolean] True if target validation fails as expected
|
|
185
|
+
def failure_target_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
186
|
+
input_target_in = attribute_data.dig(:target, :in)
|
|
187
|
+
return true if input_target_in.blank?
|
|
188
|
+
|
|
189
|
+
target_values = input_target_in.is_a?(Array) ? input_target_in : [input_target_in]
|
|
190
|
+
wrong_value = Servactory::TestKit::Utils::Faker.fetch_value_for(target_values.first.class)
|
|
191
|
+
|
|
192
|
+
prepared_attributes = attributes.dup
|
|
193
|
+
prepared_attributes[attribute_name] = wrong_value
|
|
194
|
+
|
|
195
|
+
input_required_message = attribute_data.fetch(:target).fetch(:message)
|
|
196
|
+
|
|
197
|
+
# If message is a Proc, we can't easily evaluate it in the test context
|
|
198
|
+
# (it may require runtime args like input:, value:), so skip message comparison
|
|
199
|
+
if input_required_message.is_a?(Proc)
|
|
200
|
+
return expect_failure_with!(prepared_attributes, :skip_message_check)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if input_required_message.nil?
|
|
204
|
+
input_required_message = I18n.t(
|
|
205
|
+
"#{i18n_root_key}.inputs.validations.must.dynamic_options.target.default",
|
|
206
|
+
service_class_name: described_class.name,
|
|
207
|
+
input_name: attribute_name,
|
|
208
|
+
expected_target: input_target_in.inspect,
|
|
209
|
+
value: wrong_value.inspect
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
expect_failure_with!(prepared_attributes, input_required_message)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Calls service and verifies it fails with expected message.
|
|
217
|
+
#
|
|
218
|
+
# @param prepared_attributes [Hash] Attributes to pass to service
|
|
219
|
+
# @param expected_message [String, Symbol, nil] Expected error message
|
|
220
|
+
# @return [Boolean] True if service fails with expected message
|
|
221
|
+
def expect_failure_with!(prepared_attributes, expected_message)
|
|
222
|
+
described_class.call!(prepared_attributes).success?
|
|
223
|
+
rescue Servactory::Exceptions::Input => e
|
|
224
|
+
return true if expected_message == :skip_message_check # Just verify error was raised
|
|
225
|
+
return false if expected_message.blank?
|
|
226
|
+
|
|
227
|
+
message_to_compare = expected_message.is_a?(Proc) ? expected_message.call : expected_message
|
|
228
|
+
message_to_compare.to_s.casecmp(e.message.to_s).zero?
|
|
229
|
+
rescue Servactory::Exceptions::Internal, Servactory::Exceptions::Output
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Generates a value that is outside the inclusion set.
|
|
234
|
+
# Handles Range, Array, and scalar values.
|
|
235
|
+
#
|
|
236
|
+
# @param inclusion_value [Range, Array, Object] The inclusion constraint
|
|
237
|
+
# @return [Object] A value guaranteed to be outside the inclusion set
|
|
238
|
+
def generate_wrong_value_for_inclusion(inclusion_value)
|
|
239
|
+
case inclusion_value
|
|
240
|
+
when Range
|
|
241
|
+
generate_wrong_value_for_range(inclusion_value)
|
|
242
|
+
when Array
|
|
243
|
+
Servactory::TestKit::Utils::Faker.fetch_value_for(inclusion_value.first.class)
|
|
244
|
+
else
|
|
245
|
+
Servactory::TestKit::Utils::Faker.fetch_value_for(inclusion_value.class)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Generates a value outside a Range.
|
|
250
|
+
#
|
|
251
|
+
# @param range [Range] The range constraint
|
|
252
|
+
# @return [Object] A value outside the range
|
|
253
|
+
def generate_wrong_value_for_range(range)
|
|
254
|
+
if range.begin.nil?
|
|
255
|
+
# Beginless range (..100): use value above end
|
|
256
|
+
range.end + 1
|
|
257
|
+
elsif range.end.nil?
|
|
258
|
+
# Endless range (1..): use value below begin
|
|
259
|
+
range.begin - 1
|
|
260
|
+
else
|
|
261
|
+
# Normal range: use value above end
|
|
262
|
+
range.exclude_end? ? range.end : range.end + 1
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating Array/Hash element types.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that an Array or Hash attribute specifies the expected
|
|
14
|
+
# element types via the `consists_of` option. Used with collection
|
|
15
|
+
# attributes to verify their content type constraints.
|
|
16
|
+
#
|
|
17
|
+
# ## Usage
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# it { is_expected.to have_service_input(:items).type(Array).consists_of(Item) }
|
|
21
|
+
# it { is_expected.to have_service_input(:users).type(Array).consists_of(User, Admin) }
|
|
22
|
+
# it { is_expected.to have_service_internal(:data).type(Hash).consists_of(String) }
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# ## Note
|
|
26
|
+
#
|
|
27
|
+
# Requires the `:types` option to be set first (via `.type` chain).
|
|
28
|
+
class ConsistsOfSubmatcher < Base::Submatcher
|
|
29
|
+
# Option name in attribute data
|
|
30
|
+
OPTION_NAME = :consists_of
|
|
31
|
+
# Key for the type value within the option
|
|
32
|
+
OPTION_BODY_KEY = :type
|
|
33
|
+
|
|
34
|
+
# Creates a new consists_of submatcher.
|
|
35
|
+
#
|
|
36
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
37
|
+
# @param consists_of_types [Array<Class>] Expected element types
|
|
38
|
+
# @return [ConsistsOfSubmatcher] New submatcher instance
|
|
39
|
+
def initialize(context, consists_of_types)
|
|
40
|
+
super(context)
|
|
41
|
+
@consists_of_types = consists_of_types
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns description for RSpec output.
|
|
45
|
+
#
|
|
46
|
+
# @return [String] Human-readable description with element types
|
|
47
|
+
def description
|
|
48
|
+
"consists_of: #{consists_of_types.map(&:name).join(', ')}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
# Checks if the element types match expected types.
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean] True if element types match (order-independent)
|
|
56
|
+
def passes?
|
|
57
|
+
attribute_consists_of = attribute_data.fetch(OPTION_NAME)
|
|
58
|
+
@actual_types = Array(attribute_consists_of.fetch(OPTION_BODY_KEY))
|
|
59
|
+
|
|
60
|
+
@actual_types.difference(consists_of_types).empty? &&
|
|
61
|
+
consists_of_types.difference(@actual_types).empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Builds the failure message for consists_of validation.
|
|
65
|
+
#
|
|
66
|
+
# @return [String] Failure message with expected vs actual types
|
|
67
|
+
def build_failure_message
|
|
68
|
+
<<~MESSAGE
|
|
69
|
+
should be consists_of #{consists_of_types.map(&:name).join(', ')}
|
|
70
|
+
|
|
71
|
+
expected #{consists_of_types.inspect}
|
|
72
|
+
got #{@actual_types.inspect}
|
|
73
|
+
MESSAGE
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
attr_reader :consists_of_types
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|