servactory 2.16.1 → 3.0.0.rc1
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/stroma/dsl.rb +118 -0
- data/lib/servactory/stroma/entry.rb +32 -0
- data/lib/servactory/stroma/exceptions/base.rb +45 -0
- data/lib/servactory/stroma/exceptions/invalid_hook_type.rb +29 -0
- data/lib/servactory/stroma/exceptions/key_already_registered.rb +32 -0
- data/lib/servactory/stroma/exceptions/registry_frozen.rb +33 -0
- data/lib/servactory/stroma/exceptions/registry_not_finalized.rb +33 -0
- data/lib/servactory/stroma/exceptions/unknown_hook_target.rb +39 -0
- data/lib/servactory/stroma/hooks/applier.rb +63 -0
- data/lib/servactory/stroma/hooks/collection.rb +103 -0
- data/lib/servactory/stroma/hooks/factory.rb +80 -0
- data/lib/servactory/stroma/hooks/hook.rb +74 -0
- data/lib/servactory/stroma/registry.rb +94 -0
- data/lib/servactory/stroma/settings/collection.rb +90 -0
- data/lib/servactory/stroma/settings/registry_settings.rb +88 -0
- data/lib/servactory/stroma/settings/setting.rb +113 -0
- data/lib/servactory/stroma/state.rb +59 -0
- 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 +252 -0
- data/lib/servactory/version.rb +4 -4
- data/lib/servactory.rb +4 -0
- metadata +73 -19
- 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,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# RSpec argument matchers with service-friendly aliases.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Provides semantic aliases for RSpec's argument matchers that read
|
|
12
|
+
# more naturally in service testing contexts.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# **Fluent API:**
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# allow_service(Service).with(including(amount: 100)).succeeds(result: "ok")
|
|
20
|
+
# allow_service(Service).with(excluding(secret: anything)).succeeds(result: "ok")
|
|
21
|
+
# allow_service(Service).with(any_inputs).succeeds(result: "ok")
|
|
22
|
+
# allow_service(EmptyService).with(no_inputs).succeeds(result: "ok")
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# **Legacy API:**
|
|
26
|
+
#
|
|
27
|
+
# ```ruby
|
|
28
|
+
# allow_service_as_success!(Service, with: including(amount: 100)) { { result: "ok" } }
|
|
29
|
+
# allow_service_as_success!(Service, with: excluding(secret: anything)) { { result: "ok" } }
|
|
30
|
+
# allow_service_as_success!(Service, with: any_inputs) { { result: "ok" } }
|
|
31
|
+
# allow_service_as_success!(EmptyService, with: no_inputs) { { result: "ok" } }
|
|
32
|
+
# ```
|
|
33
|
+
module ArgumentMatchers
|
|
34
|
+
# Matches a hash containing specified key-value pairs.
|
|
35
|
+
#
|
|
36
|
+
# Alias for RSpec's `hash_including` with service-friendly naming.
|
|
37
|
+
#
|
|
38
|
+
# @param hash [Hash] Expected key-value pairs
|
|
39
|
+
# @return [RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher]
|
|
40
|
+
#
|
|
41
|
+
# @example Fluent API
|
|
42
|
+
# allow_service(Service).with(including(amount: 100)).succeeds(result: "ok")
|
|
43
|
+
#
|
|
44
|
+
# @example Legacy API
|
|
45
|
+
# allow_service_as_success!(Service, with: including(amount: 100)) { { result: "ok" } }
|
|
46
|
+
def including(hash)
|
|
47
|
+
hash_including(hash)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Matches a hash NOT containing specified key-value pairs.
|
|
51
|
+
#
|
|
52
|
+
# Alias for RSpec's `hash_excluding` with service-friendly naming.
|
|
53
|
+
#
|
|
54
|
+
# @param hash [Hash] Key-value pairs to exclude
|
|
55
|
+
# @return [RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher]
|
|
56
|
+
#
|
|
57
|
+
# @example Fluent API
|
|
58
|
+
# allow_service(Service).with(excluding(secret: anything)).succeeds(result: "ok")
|
|
59
|
+
#
|
|
60
|
+
# @example Legacy API
|
|
61
|
+
# allow_service_as_success!(Service, with: excluding(secret: anything)) { { result: "ok" } }
|
|
62
|
+
def excluding(hash)
|
|
63
|
+
hash_excluding(hash)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Matches any service inputs (wildcard matcher).
|
|
67
|
+
#
|
|
68
|
+
# Useful for "don't care" scenarios where input values don't matter.
|
|
69
|
+
#
|
|
70
|
+
# @return [RSpec::Mocks::ArgumentMatchers::AnyArgMatcher]
|
|
71
|
+
#
|
|
72
|
+
# @example Fluent API
|
|
73
|
+
# allow_service(Service).with(any_inputs).succeeds(result: "ok")
|
|
74
|
+
#
|
|
75
|
+
# @example Legacy API
|
|
76
|
+
# allow_service_as_success!(Service, with: any_inputs) { { result: "ok" } }
|
|
77
|
+
def any_inputs
|
|
78
|
+
anything
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Matches no arguments (for services without inputs).
|
|
82
|
+
#
|
|
83
|
+
# @return [RSpec::Mocks::ArgumentMatchers::NoArgsMatcher]
|
|
84
|
+
#
|
|
85
|
+
# @example Fluent API
|
|
86
|
+
# allow_service(EmptyService).with(no_inputs).succeeds(result: "ok")
|
|
87
|
+
#
|
|
88
|
+
# @example Legacy API
|
|
89
|
+
# allow_service_as_success!(EmptyService, with: no_inputs) { { result: "ok" } }
|
|
90
|
+
def no_inputs
|
|
91
|
+
no_args
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
module Concerns
|
|
8
|
+
# Concern providing error message builders for service mock helpers.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# ErrorMessages provides standardized, helpful error messages for common
|
|
13
|
+
# issues in service mocking. Each message includes context about what went
|
|
14
|
+
# wrong, hints for fixing the issue, and code examples.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Include in helper classes that need to report errors:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class ServiceMockBuilder
|
|
22
|
+
# include Concerns::ErrorMessages
|
|
23
|
+
#
|
|
24
|
+
# def validate!
|
|
25
|
+
# raise ArgumentError, missing_exception_for_failure_message(service_class)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# ## Message Categories
|
|
31
|
+
#
|
|
32
|
+
# - Service class validation errors
|
|
33
|
+
# - Block return value errors
|
|
34
|
+
# - Output validation errors
|
|
35
|
+
# - Type mismatch errors
|
|
36
|
+
# - Result type configuration errors
|
|
37
|
+
# - Exception configuration errors
|
|
38
|
+
module ErrorMessages
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Builds error message for invalid service class.
|
|
42
|
+
#
|
|
43
|
+
# @param given [Object] The invalid value that was provided
|
|
44
|
+
# @return [String] Error message with hint and example
|
|
45
|
+
def invalid_service_class_message(given)
|
|
46
|
+
<<~MESSAGE.squish
|
|
47
|
+
Invalid service class provided to service mock helper.
|
|
48
|
+
Expected a class responding to `.call` and `.call!`,
|
|
49
|
+
got: #{given.inspect} (#{given.class.name}).
|
|
50
|
+
Hint: Ensure you're passing the service class, not an instance.
|
|
51
|
+
Example: allow_service(MyService).succeeds(result: "value")
|
|
52
|
+
MESSAGE
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Builds error message for invalid block return value.
|
|
56
|
+
#
|
|
57
|
+
# @param given [Object] The actual return value
|
|
58
|
+
# @param expected_type [String] Description of expected type
|
|
59
|
+
# @return [String] Error message with example
|
|
60
|
+
def invalid_block_return_message(given, expected_type)
|
|
61
|
+
<<~MESSAGE.squish
|
|
62
|
+
Invalid block return value in service mock helper.
|
|
63
|
+
Expected: #{expected_type},
|
|
64
|
+
got: #{given.class.name}.
|
|
65
|
+
Example for success: allow_service_as_success!(MyService) { { user_id: 123 } }
|
|
66
|
+
MESSAGE
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Builds error message for unknown output names.
|
|
70
|
+
#
|
|
71
|
+
# @param service_class [Class] The service class
|
|
72
|
+
# @param unknown_outputs [Array<Symbol>] Outputs not defined in service
|
|
73
|
+
# @param defined_outputs [Array<Symbol>] Valid output names
|
|
74
|
+
# @return [String] Error message with hint
|
|
75
|
+
def unknown_outputs_message(service_class:, unknown_outputs:, defined_outputs:)
|
|
76
|
+
<<~MESSAGE.squish
|
|
77
|
+
Unknown output(s) for #{service_class.name}:
|
|
78
|
+
provided: #{unknown_outputs.map(&:inspect).join(', ')},
|
|
79
|
+
defined: #{defined_outputs.map(&:inspect).join(', ')}.
|
|
80
|
+
Hint: Check that the output names match the service definition.
|
|
81
|
+
MESSAGE
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Builds error message for output type mismatch.
|
|
85
|
+
#
|
|
86
|
+
# @param service_class [Class] The service class
|
|
87
|
+
# @param output_name [Symbol] Name of the mismatched output
|
|
88
|
+
# @param expected_types [Array<Class>] Expected type classes
|
|
89
|
+
# @param actual_value [Object] The value with wrong type
|
|
90
|
+
# @return [String] Error message with hint
|
|
91
|
+
def type_mismatch_message(service_class:, output_name:, expected_types:, actual_value:)
|
|
92
|
+
<<~MESSAGE.squish
|
|
93
|
+
Type mismatch for output :#{output_name} in #{service_class.name}.
|
|
94
|
+
Expected: #{expected_types.map(&:name).join(' or ')},
|
|
95
|
+
got: #{actual_value.class.name} (#{actual_value.inspect}).
|
|
96
|
+
Hint: Ensure the mocked value matches the expected type.
|
|
97
|
+
MESSAGE
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Builds error message for missing result type.
|
|
101
|
+
#
|
|
102
|
+
# @return [String] Error message with example
|
|
103
|
+
def missing_result_type_message
|
|
104
|
+
<<~MESSAGE.squish
|
|
105
|
+
Result type not specified.
|
|
106
|
+
Use .succeeds() or .fails() to specify the mock result type.
|
|
107
|
+
Example: allow_service(MyService).succeeds(data: "value")
|
|
108
|
+
MESSAGE
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Builds error message for failure mock missing exception.
|
|
112
|
+
#
|
|
113
|
+
# @param service_class [Class] The service class
|
|
114
|
+
# @return [String] Error message with example showing full signature
|
|
115
|
+
def missing_exception_for_failure_message(service_class)
|
|
116
|
+
<<~MESSAGE.squish
|
|
117
|
+
Exception is required for failure mock of #{service_class.name}.
|
|
118
|
+
Servactory supports custom exception classes via configuration,
|
|
119
|
+
so you must explicitly specify the exception.
|
|
120
|
+
Example:
|
|
121
|
+
allow_service(#{service_class.name})
|
|
122
|
+
.fails(message: "Error message")
|
|
123
|
+
Full signature: .fails(type: :custom_type, message: "...", meta: { key: :value })
|
|
124
|
+
MESSAGE
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Builds error message for wrong exception type.
|
|
128
|
+
#
|
|
129
|
+
# @param service_class [Class] The service class
|
|
130
|
+
# @param expected_class [Class] The configured failure class
|
|
131
|
+
# @param actual_class [Class] The provided exception's class
|
|
132
|
+
# @return [String] Error message with hint and example
|
|
133
|
+
def invalid_exception_type_message(service_class:, expected_class:, actual_class:)
|
|
134
|
+
<<~MESSAGE.squish
|
|
135
|
+
Invalid exception type for failure mock of #{service_class.name}.
|
|
136
|
+
Expected: instance of #{expected_class.name} (configured failure_class),
|
|
137
|
+
got: #{actual_class.name}.
|
|
138
|
+
Hint: Use the service's configured failure class or its subclass.
|
|
139
|
+
Example:
|
|
140
|
+
allow_service(#{service_class.name})
|
|
141
|
+
.fails(#{expected_class.name}, message: "Error message")
|
|
142
|
+
Full signature: .fails(MyException, type: :custom_type, message: "...", meta: { key: :value })
|
|
143
|
+
MESSAGE
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Builds error message for unknown input names.
|
|
147
|
+
#
|
|
148
|
+
# @param service_class [Class] The service class
|
|
149
|
+
# @param unknown_inputs [Array<Symbol>] Inputs not defined in service
|
|
150
|
+
# @param defined_inputs [Array<Symbol>] Valid input names
|
|
151
|
+
# @return [String] Error message with hint
|
|
152
|
+
def unknown_inputs_message(service_class:, unknown_inputs:, defined_inputs:)
|
|
153
|
+
<<~MESSAGE.squish
|
|
154
|
+
Unknown input(s) for #{service_class.name}:
|
|
155
|
+
provided: #{unknown_inputs.map(&:inspect).join(', ')},
|
|
156
|
+
defined: #{defined_inputs.map(&:inspect).join(', ')}.
|
|
157
|
+
Hint: Check that the input names match the service definition.
|
|
158
|
+
MESSAGE
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Builds error message when no_inputs used but service has required inputs.
|
|
162
|
+
#
|
|
163
|
+
# @param service_class [Class] The service class
|
|
164
|
+
# @param required_inputs [Array<Symbol>] Required input names
|
|
165
|
+
# @return [String] Error message with hint
|
|
166
|
+
def no_inputs_but_required_message(service_class:, required_inputs:)
|
|
167
|
+
<<~MESSAGE.squish
|
|
168
|
+
Service #{service_class.name} has required inputs,
|
|
169
|
+
but no_inputs matcher was used.
|
|
170
|
+
Required inputs: #{required_inputs.map(&:inspect).join(', ')}.
|
|
171
|
+
Hint: Use with({...}) with the required inputs instead of no_inputs.
|
|
172
|
+
MESSAGE
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
module Concerns
|
|
8
|
+
# Concern providing validation for Servactory service classes.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# ServiceClassValidation ensures that values passed to service mock helpers
|
|
13
|
+
# are valid Servactory service classes. It checks for the required interface
|
|
14
|
+
# methods and provides helpful error messages when validation fails.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Include in helper classes that accept service classes:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# class ServiceMockBuilder
|
|
22
|
+
# include Concerns::ServiceClassValidation
|
|
23
|
+
#
|
|
24
|
+
# def initialize(service_class)
|
|
25
|
+
# validate_service_class!(service_class)
|
|
26
|
+
# @service_class = service_class
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# ```
|
|
30
|
+
#
|
|
31
|
+
# ## Validation Rules
|
|
32
|
+
#
|
|
33
|
+
# A valid service class must:
|
|
34
|
+
# - Be a Class (not instance or module)
|
|
35
|
+
# - Respond to `.call` method
|
|
36
|
+
# - Respond to `.call!` method
|
|
37
|
+
# - Respond to `.info` method (for introspection)
|
|
38
|
+
module ServiceClassValidation
|
|
39
|
+
include ErrorMessages
|
|
40
|
+
|
|
41
|
+
# Error raised when an invalid service class is provided.
|
|
42
|
+
class InvalidServiceClassError < ArgumentError; end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Validates that the given value is a valid Servactory service class.
|
|
47
|
+
#
|
|
48
|
+
# @param service_class [Object] The value to validate
|
|
49
|
+
# @return [void]
|
|
50
|
+
# @raise [InvalidServiceClassError] If validation fails
|
|
51
|
+
def validate_service_class!(service_class)
|
|
52
|
+
return if valid_service_class?(service_class)
|
|
53
|
+
|
|
54
|
+
raise InvalidServiceClassError, invalid_service_class_message(service_class)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Checks if the given value is a valid Servactory service class.
|
|
58
|
+
#
|
|
59
|
+
# @param service_class [Object] The value to check
|
|
60
|
+
# @return [Boolean] True if valid Servactory service class
|
|
61
|
+
def valid_service_class?(service_class)
|
|
62
|
+
return false unless service_class.is_a?(Class)
|
|
63
|
+
return false unless service_class.respond_to?(:call)
|
|
64
|
+
return false unless service_class.respond_to?(:call!)
|
|
65
|
+
return false unless service_class.respond_to?(:info)
|
|
66
|
+
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Fluent API for mocking Servactory services in RSpec tests.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Provides modern fluent builder interface for configuring service mocks
|
|
12
|
+
# with chainable method calls. This is the recommended API for new tests.
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# Include in RSpec configuration via the main Helpers module:
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# RSpec.configure do |config|
|
|
20
|
+
# config.include Servactory::TestKit::Rspec::Helpers, type: :service
|
|
21
|
+
# end
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Available Methods
|
|
25
|
+
#
|
|
26
|
+
# - `allow_service(ServiceClass)` - mock `.call` method (returns Result)
|
|
27
|
+
# - `allow_service!(ServiceClass)` - mock `.call!` method (raises on failure)
|
|
28
|
+
#
|
|
29
|
+
# ## Examples
|
|
30
|
+
#
|
|
31
|
+
# ```ruby
|
|
32
|
+
# # Mock successful service call with outputs
|
|
33
|
+
# allow_service(PaymentService)
|
|
34
|
+
# .succeeds(transaction_id: "txn_123", status: :completed)
|
|
35
|
+
#
|
|
36
|
+
# # Mock with input matching
|
|
37
|
+
# allow_service(PaymentService)
|
|
38
|
+
# .with(amount: 100)
|
|
39
|
+
# .succeeds(transaction_id: "txn_100")
|
|
40
|
+
#
|
|
41
|
+
# # Mock failure
|
|
42
|
+
# allow_service(PaymentService)
|
|
43
|
+
# .fails(type: :payment_declined, message: "Card declined")
|
|
44
|
+
#
|
|
45
|
+
# # Sequential returns
|
|
46
|
+
# allow_service(PaymentService)
|
|
47
|
+
# .succeeds(status: :pending)
|
|
48
|
+
# .then_succeeds(status: :completed)
|
|
49
|
+
# ```
|
|
50
|
+
#
|
|
51
|
+
# @see ServiceMockBuilder for full fluent API documentation
|
|
52
|
+
module Fluent
|
|
53
|
+
# Start building a service mock with fluent API for .call method.
|
|
54
|
+
#
|
|
55
|
+
# When service fails, returns Result with `.error` attribute.
|
|
56
|
+
#
|
|
57
|
+
# @param service_class [Class] The service class to mock
|
|
58
|
+
# @return [ServiceMockBuilder] Builder for fluent configuration
|
|
59
|
+
#
|
|
60
|
+
# @example Success mock
|
|
61
|
+
# allow_service(PaymentService)
|
|
62
|
+
# .succeeds(transaction_id: "txn_123", status: :completed)
|
|
63
|
+
#
|
|
64
|
+
# @example Success with input matching
|
|
65
|
+
# allow_service(PaymentService)
|
|
66
|
+
# .with(amount: 100)
|
|
67
|
+
# .succeeds(transaction_id: "txn_123")
|
|
68
|
+
#
|
|
69
|
+
# @example Failure mock
|
|
70
|
+
# allow_service(PaymentService)
|
|
71
|
+
# .fails(type: :payment_declined, message: "Card declined")
|
|
72
|
+
#
|
|
73
|
+
# @example Sequential returns
|
|
74
|
+
# allow_service(PaymentService)
|
|
75
|
+
# .succeeds(status: :pending)
|
|
76
|
+
# .then_succeeds(status: :completed)
|
|
77
|
+
# .then_fails(type: :timeout, message: "Request timed out")
|
|
78
|
+
#
|
|
79
|
+
def allow_service(service_class)
|
|
80
|
+
Helpers::ServiceMockBuilder.new(service_class, method_type: :call, rspec_context: self)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Start building a service mock with fluent API for .call! method.
|
|
84
|
+
#
|
|
85
|
+
# When service fails, raises exception.
|
|
86
|
+
#
|
|
87
|
+
# @param service_class [Class] The service class to mock
|
|
88
|
+
# @return [ServiceMockBuilder] Builder for fluent configuration
|
|
89
|
+
#
|
|
90
|
+
# @example Success mock for call!
|
|
91
|
+
# allow_service!(PaymentService)
|
|
92
|
+
# .succeeds(transaction_id: "txn_123", status: :completed)
|
|
93
|
+
#
|
|
94
|
+
# @example Failure mock for call! (raises exception)
|
|
95
|
+
# allow_service!(PaymentService)
|
|
96
|
+
# .fails(type: :payment_declined, message: "Insufficient funds")
|
|
97
|
+
#
|
|
98
|
+
# @example Sequential returns
|
|
99
|
+
# allow_service!(RetryService)
|
|
100
|
+
# .succeeds(status: :pending)
|
|
101
|
+
# .then_fails(type: :timeout, message: "Request timed out")
|
|
102
|
+
#
|
|
103
|
+
def allow_service!(service_class)
|
|
104
|
+
Helpers::ServiceMockBuilder.new(service_class, method_type: :call!, rspec_context: self)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Validates mock input values against service definitions.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Ensures that mocked input arguments match the service's input
|
|
12
|
+
# definitions in terms of names. Called automatically when using
|
|
13
|
+
# `.with()` on the fluent builder.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# Called automatically from ServiceMockBuilder:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# allow_service(MyService)
|
|
21
|
+
# .with(user_id: 123)
|
|
22
|
+
# .succeeds(result: "ok")
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# Can also be called directly:
|
|
26
|
+
#
|
|
27
|
+
# ```ruby
|
|
28
|
+
# InputValidator.validate!(
|
|
29
|
+
# service_class: MyService,
|
|
30
|
+
# inputs_matcher: { user_id: 123 }
|
|
31
|
+
# )
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# ## Supported Matcher Types
|
|
35
|
+
#
|
|
36
|
+
# - Hash - validates all keys against service inputs
|
|
37
|
+
# - `including(hash)` - validates specified keys only
|
|
38
|
+
# - `any_inputs` - skips validation (accepts anything)
|
|
39
|
+
# - `no_inputs` - validates service has no required inputs
|
|
40
|
+
# - `excluding(hash)` - skips validation (cannot validate exclusion)
|
|
41
|
+
class InputValidator
|
|
42
|
+
include Concerns::ErrorMessages
|
|
43
|
+
|
|
44
|
+
# Error raised when validation fails
|
|
45
|
+
class ValidationError < StandardError; end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# Validates inputs and raises on failure.
|
|
49
|
+
#
|
|
50
|
+
# @param service_class [Class] The service class
|
|
51
|
+
# @param inputs_matcher [Hash, Object] Input values or matcher
|
|
52
|
+
# @raise [ValidationError] If validation fails
|
|
53
|
+
# @return [void]
|
|
54
|
+
def validate!(service_class:, inputs_matcher:)
|
|
55
|
+
new(service_class:, inputs_matcher:).validate!
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates a new validator instance.
|
|
60
|
+
#
|
|
61
|
+
# @param service_class [Class] The service class
|
|
62
|
+
# @param inputs_matcher [Hash, Object] Input values or matcher
|
|
63
|
+
# @return [InputValidator] New validator
|
|
64
|
+
def initialize(service_class:, inputs_matcher:)
|
|
65
|
+
@service_class = service_class
|
|
66
|
+
@inputs_matcher = inputs_matcher
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Runs validation based on matcher type.
|
|
70
|
+
#
|
|
71
|
+
# @raise [ValidationError] If validation fails
|
|
72
|
+
# @return [void]
|
|
73
|
+
def validate!
|
|
74
|
+
keys_to_validate = extract_keys_for_validation
|
|
75
|
+
|
|
76
|
+
return if keys_to_validate.nil?
|
|
77
|
+
|
|
78
|
+
if keys_to_validate == :no_args
|
|
79
|
+
validate_service_has_no_required_inputs!
|
|
80
|
+
else
|
|
81
|
+
validate_input_names!(keys_to_validate)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Extracts keys to validate based on matcher type.
|
|
88
|
+
#
|
|
89
|
+
# @return [Array<Symbol>, Symbol, nil] Keys to validate, :no_args marker, or nil to skip
|
|
90
|
+
def extract_keys_for_validation # rubocop:disable Metrics/MethodLength
|
|
91
|
+
case @inputs_matcher
|
|
92
|
+
when Hash
|
|
93
|
+
@inputs_matcher.keys
|
|
94
|
+
when RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
|
|
95
|
+
extract_hash_including_keys
|
|
96
|
+
when RSpec::Mocks::ArgumentMatchers::NoArgsMatcher
|
|
97
|
+
:no_args
|
|
98
|
+
when RSpec::Mocks::ArgumentMatchers::AnyArgMatcher,
|
|
99
|
+
RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Extracts keys from HashIncludingMatcher.
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<Symbol>, nil] Keys from the matcher or nil
|
|
107
|
+
def extract_hash_including_keys
|
|
108
|
+
@inputs_matcher.instance_variable_get(:@expected)&.keys
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Validates all input names are defined in service.
|
|
114
|
+
#
|
|
115
|
+
# @param provided_keys [Array<Symbol>] Keys to validate
|
|
116
|
+
# @raise [ValidationError] If unknown input names provided
|
|
117
|
+
# @return [void]
|
|
118
|
+
def validate_input_names!(provided_keys)
|
|
119
|
+
defined_inputs = @service_class.info.inputs.keys
|
|
120
|
+
unknown_inputs = provided_keys - defined_inputs
|
|
121
|
+
|
|
122
|
+
return if unknown_inputs.empty?
|
|
123
|
+
|
|
124
|
+
raise ValidationError, unknown_inputs_message(
|
|
125
|
+
service_class: @service_class,
|
|
126
|
+
unknown_inputs:,
|
|
127
|
+
defined_inputs:
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Validates service has no required inputs when no_inputs used.
|
|
132
|
+
#
|
|
133
|
+
# @raise [ValidationError] If service has required inputs
|
|
134
|
+
# @return [void]
|
|
135
|
+
def validate_service_has_no_required_inputs!
|
|
136
|
+
required_inputs = @service_class.info.inputs.reject { |_, v| v[:required] == false }.keys
|
|
137
|
+
|
|
138
|
+
return if required_inputs.empty?
|
|
139
|
+
|
|
140
|
+
raise ValidationError, no_inputs_but_required_message(
|
|
141
|
+
service_class: @service_class,
|
|
142
|
+
required_inputs:
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|