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,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
# RSpec matcher for validating service result output values.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Validates that a service result contains an expected output value with
|
|
12
|
+
# specific type, nested attributes, and content. Unlike input/internal
|
|
13
|
+
# matchers that validate definitions, this matcher validates actual runtime
|
|
14
|
+
# output values on a service result object.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# RSpec.describe MyService, type: :service do
|
|
20
|
+
# let(:result) { described_class.call(user_id: 123) }
|
|
21
|
+
#
|
|
22
|
+
# it "returns expected output" do
|
|
23
|
+
# expect(result).to have_service_output(:user)
|
|
24
|
+
# .instance_of(User)
|
|
25
|
+
# .contains(name: "John")
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# it "validates nested attributes" do
|
|
29
|
+
# expect(result).to have_service_output(:data)
|
|
30
|
+
# .nested(:settings, :theme)
|
|
31
|
+
# .contains("dark")
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
# ```
|
|
35
|
+
#
|
|
36
|
+
# ## Chain Methods
|
|
37
|
+
#
|
|
38
|
+
# - `.instance_of(Class)` - validates output is instance of class
|
|
39
|
+
# - `.nested(*methods)` - traverses nested attributes before comparison
|
|
40
|
+
# - `.contains(value)` - validates output value or structure
|
|
41
|
+
#
|
|
42
|
+
# ## Value Comparison
|
|
43
|
+
#
|
|
44
|
+
# The `.contains` method uses type-aware comparison:
|
|
45
|
+
# - Array - uses RSpec's `contain_exactly`
|
|
46
|
+
# - Hash - uses RSpec's `match`
|
|
47
|
+
# - Boolean - uses RSpec's `equal` (identity)
|
|
48
|
+
# - nil - uses RSpec's `be_nil`
|
|
49
|
+
# - Other - uses RSpec's `eq` (equality)
|
|
50
|
+
class HaveServiceOutputMatcher # rubocop:disable Metrics/ClassLength
|
|
51
|
+
include RSpec::Matchers::Composable
|
|
52
|
+
|
|
53
|
+
# Creates a new output matcher for the given output name.
|
|
54
|
+
#
|
|
55
|
+
# @param output_name [Symbol] The name of the output to validate
|
|
56
|
+
# @return [HaveServiceOutputMatcher] New matcher instance
|
|
57
|
+
def initialize(output_name)
|
|
58
|
+
@output_name = output_name
|
|
59
|
+
@instance_of_class = nil
|
|
60
|
+
@nested_methods = []
|
|
61
|
+
@expected_value = nil
|
|
62
|
+
@value_defined = false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Indicates this matcher does not support block expectations.
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean] Always false
|
|
68
|
+
def supports_block_expectations?
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Performs the match against the actual service result.
|
|
73
|
+
#
|
|
74
|
+
# @param actual [Servactory::Result] The service result to validate
|
|
75
|
+
# @return [Boolean] True if all checks pass
|
|
76
|
+
def matches?(actual)
|
|
77
|
+
@actual = actual
|
|
78
|
+
@given_value = actual.public_send(output_name)
|
|
79
|
+
|
|
80
|
+
check_instance_of && check_nested && check_contains
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns a description of what this matcher validates.
|
|
84
|
+
#
|
|
85
|
+
# @return [String] Human-readable matcher description
|
|
86
|
+
def description
|
|
87
|
+
"service output #{output_name}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns the failure message when the match fails.
|
|
91
|
+
#
|
|
92
|
+
# @return [String] Detailed failure message from RSpec matcher
|
|
93
|
+
def failure_message
|
|
94
|
+
match_for_failure
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns the failure message for negated expectations.
|
|
98
|
+
#
|
|
99
|
+
# @return [String] Negated failure message
|
|
100
|
+
def failure_message_when_negated
|
|
101
|
+
"Expected result not to have output #{output_name}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Chain Methods
|
|
105
|
+
# -------------
|
|
106
|
+
|
|
107
|
+
# Specifies the expected class of the output value.
|
|
108
|
+
#
|
|
109
|
+
# @param class_or_name [Class, String] Expected class or class name
|
|
110
|
+
# @return [self] For method chaining
|
|
111
|
+
def instance_of(class_or_name)
|
|
112
|
+
@instance_of_class = Servactory::Utils.constantize_class(class_or_name)
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Specifies nested method chain to traverse before comparison.
|
|
117
|
+
#
|
|
118
|
+
# Allows validating deeply nested attributes by chaining method calls
|
|
119
|
+
# on the output value before performing the final comparison.
|
|
120
|
+
#
|
|
121
|
+
# @param methods [Array<Symbol>] Method names to call in sequence
|
|
122
|
+
# @return [self] For method chaining
|
|
123
|
+
#
|
|
124
|
+
# @example Validate nested attribute
|
|
125
|
+
# expect(result).to have_output(:user).nested(:profile, :settings).contains(theme: "dark")
|
|
126
|
+
def nested(*methods)
|
|
127
|
+
@nested_methods = methods
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Specifies the expected value or structure of the output.
|
|
132
|
+
#
|
|
133
|
+
# @param value [Object] Expected value (uses type-aware comparison)
|
|
134
|
+
# @return [self] For method chaining
|
|
135
|
+
def contains(value)
|
|
136
|
+
@expected_value = value
|
|
137
|
+
@value_defined = true
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
attr_reader :output_name,
|
|
144
|
+
:actual,
|
|
145
|
+
:given_value,
|
|
146
|
+
:instance_of_class,
|
|
147
|
+
:nested_methods,
|
|
148
|
+
:expected_value
|
|
149
|
+
|
|
150
|
+
# Validates output value is an instance of expected class.
|
|
151
|
+
#
|
|
152
|
+
# @return [Boolean] True if class check passes or no class specified
|
|
153
|
+
def check_instance_of # rubocop:disable Naming/PredicateMethod
|
|
154
|
+
return true unless instance_of_class
|
|
155
|
+
|
|
156
|
+
matcher = RSpec::Matchers::BuiltIn::BeAnInstanceOf.new(instance_of_class)
|
|
157
|
+
matcher.matches?(@given_value)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Traverses nested methods to get the value for comparison.
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean] Always true after traversing
|
|
163
|
+
def check_nested # rubocop:disable Naming/PredicateMethod
|
|
164
|
+
return true if nested_methods.empty?
|
|
165
|
+
|
|
166
|
+
nested_methods.each do |method_name|
|
|
167
|
+
next unless @given_value.respond_to?(method_name)
|
|
168
|
+
|
|
169
|
+
@given_value = @given_value.public_send(method_name)
|
|
170
|
+
end
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Validates output value matches expected using type-aware comparison.
|
|
175
|
+
#
|
|
176
|
+
# @return [Boolean] True if value matches or no value specified
|
|
177
|
+
def check_contains # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
|
|
178
|
+
return true unless @value_defined
|
|
179
|
+
|
|
180
|
+
matcher = case expected_value
|
|
181
|
+
when Array
|
|
182
|
+
RSpec::Matchers::BuiltIn::ContainExactly.new(expected_value)
|
|
183
|
+
when Hash
|
|
184
|
+
RSpec::Matchers::BuiltIn::Match.new(expected_value)
|
|
185
|
+
when TrueClass, FalseClass
|
|
186
|
+
RSpec::Matchers::BuiltIn::Equal.new(expected_value)
|
|
187
|
+
when NilClass
|
|
188
|
+
RSpec::Matchers::BuiltIn::BeNil.new(expected_value)
|
|
189
|
+
else
|
|
190
|
+
RSpec::Matchers::BuiltIn::Eq.new(expected_value)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
matcher.matches?(@given_value)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Builds detailed failure message by re-running all checks.
|
|
197
|
+
#
|
|
198
|
+
# @return [String, Boolean] Failure message or true if no failure found
|
|
199
|
+
def match_for_failure # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
200
|
+
given_value_for_check = actual.public_send(output_name)
|
|
201
|
+
|
|
202
|
+
if instance_of_class
|
|
203
|
+
matcher = RSpec::Matchers::BuiltIn::BeAnInstanceOf.new(instance_of_class)
|
|
204
|
+
return matcher.failure_message unless matcher.matches?(given_value_for_check)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if nested_methods.present?
|
|
208
|
+
nested_methods.each do |method_name|
|
|
209
|
+
next unless given_value_for_check.respond_to?(method_name)
|
|
210
|
+
|
|
211
|
+
given_value_for_check = given_value_for_check.public_send(method_name)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
return true if !@value_defined && expected_value.nil?
|
|
216
|
+
|
|
217
|
+
matcher = case expected_value
|
|
218
|
+
when Array
|
|
219
|
+
RSpec::Matchers::BuiltIn::ContainExactly.new(expected_value)
|
|
220
|
+
when Hash
|
|
221
|
+
RSpec::Matchers::BuiltIn::Match.new(expected_value)
|
|
222
|
+
when TrueClass, FalseClass
|
|
223
|
+
RSpec::Matchers::BuiltIn::Equal.new(expected_value)
|
|
224
|
+
when NilClass
|
|
225
|
+
RSpec::Matchers::BuiltIn::BeNil.new(expected_value)
|
|
226
|
+
else
|
|
227
|
+
RSpec::Matchers::BuiltIn::Eq.new(expected_value)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
return true if matcher.matches?(given_value_for_check)
|
|
231
|
+
|
|
232
|
+
matcher.failure_message
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Result
|
|
8
|
+
# RSpec matcher for validating failed service results.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# Validates that a service result is a failure with expected error type,
|
|
13
|
+
# message, and metadata. Supports custom failure classes configured via
|
|
14
|
+
# Servactory settings.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# RSpec.describe MyService, type: :service do
|
|
20
|
+
# it "fails with validation error" do
|
|
21
|
+
# result = described_class.call(invalid: true)
|
|
22
|
+
#
|
|
23
|
+
# expect(result).to be_failure_service
|
|
24
|
+
# .type(:validation_error)
|
|
25
|
+
# .message("Invalid input provided")
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# it "fails with custom exception and meta" do
|
|
29
|
+
# result = described_class.call(bad_data: true)
|
|
30
|
+
#
|
|
31
|
+
# expect(result).to be_failure_service
|
|
32
|
+
# .with(MyCustomFailure)
|
|
33
|
+
# .type(:processing_error)
|
|
34
|
+
# .meta(field: :data, code: 422)
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
# ```
|
|
38
|
+
#
|
|
39
|
+
# ## Chain Methods
|
|
40
|
+
#
|
|
41
|
+
# - `.with(Class)` - expected custom failure class
|
|
42
|
+
# - `.type(Symbol)` - expected error type (defaults to `:base`)
|
|
43
|
+
# - `.message(String)` - expected error message
|
|
44
|
+
# - `.meta(Hash)` - expected error metadata
|
|
45
|
+
#
|
|
46
|
+
# ## Validation Steps
|
|
47
|
+
#
|
|
48
|
+
# 1. Checks result is a `Servactory::Result` instance
|
|
49
|
+
# 2. Verifies `result.success?` returns false
|
|
50
|
+
# 3. Verifies `result.failure?` returns true
|
|
51
|
+
# 4. Validates error is a `Servactory::Exceptions::Failure`
|
|
52
|
+
# 5. Validates failure class if specified via `.with`
|
|
53
|
+
# 6. Validates error type (defaults to `:base`)
|
|
54
|
+
# 7. Validates message if specified
|
|
55
|
+
# 8. Validates meta if specified
|
|
56
|
+
class BeFailureServiceMatcher # rubocop:disable Metrics/ClassLength
|
|
57
|
+
include RSpec::Matchers::Composable
|
|
58
|
+
|
|
59
|
+
# Creates a new failure matcher with empty expectations.
|
|
60
|
+
#
|
|
61
|
+
# @return [BeFailureServiceMatcher] New matcher instance
|
|
62
|
+
def initialize
|
|
63
|
+
@expected_failure_class = nil
|
|
64
|
+
@expected_type = nil
|
|
65
|
+
@expected_message = nil
|
|
66
|
+
@expected_meta = nil
|
|
67
|
+
@type_defined = false
|
|
68
|
+
@message_defined = false
|
|
69
|
+
@meta_defined = false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Indicates this matcher does not support block expectations.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] Always false
|
|
75
|
+
def supports_block_expectations?
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Performs the match against the actual service result.
|
|
80
|
+
#
|
|
81
|
+
# @param result [Servactory::Result] The service result to validate
|
|
82
|
+
# @return [Boolean] True if result is failure with matching error attributes
|
|
83
|
+
def matches?(result) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
84
|
+
@result = result
|
|
85
|
+
|
|
86
|
+
failure_class = expected_failure_class || Servactory::Exceptions::Failure
|
|
87
|
+
type = @type_defined ? expected_type : :base
|
|
88
|
+
|
|
89
|
+
matched = result.is_a?(Servactory::Result)
|
|
90
|
+
matched &&= !result.success?
|
|
91
|
+
matched &&= result.failure?
|
|
92
|
+
matched &&= result.error.is_a?(Servactory::Exceptions::Failure)
|
|
93
|
+
matched &&= result.error.is_a?(failure_class)
|
|
94
|
+
matched &&= result.error.type == type
|
|
95
|
+
matched &&= result.error.message == expected_message if @message_defined
|
|
96
|
+
matched &&= result.error.meta == expected_meta if @meta_defined
|
|
97
|
+
|
|
98
|
+
matched
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns a description of what this matcher validates.
|
|
102
|
+
#
|
|
103
|
+
# @return [String] Human-readable matcher description
|
|
104
|
+
def description
|
|
105
|
+
"service failure"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns detailed failure message explaining what check failed.
|
|
109
|
+
#
|
|
110
|
+
# Checks in order: result type, failure status, error class, failure class,
|
|
111
|
+
# error type, message, and meta.
|
|
112
|
+
#
|
|
113
|
+
# @return [String] Detailed failure message with expected vs actual
|
|
114
|
+
def failure_message # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
115
|
+
unless result.is_a?(Servactory::Result)
|
|
116
|
+
return <<~MESSAGE
|
|
117
|
+
Incorrect service result:
|
|
118
|
+
|
|
119
|
+
expected Servactory::Result
|
|
120
|
+
got #{result.class.name}
|
|
121
|
+
MESSAGE
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if result.success?
|
|
125
|
+
return <<~MESSAGE
|
|
126
|
+
Incorrect service result:
|
|
127
|
+
|
|
128
|
+
expected failure
|
|
129
|
+
got success
|
|
130
|
+
MESSAGE
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unless result.error.is_a?(Servactory::Exceptions::Failure)
|
|
134
|
+
return <<~MESSAGE
|
|
135
|
+
Incorrect error object:
|
|
136
|
+
|
|
137
|
+
expected Servactory::Exceptions::Failure
|
|
138
|
+
got #{result.error.class.name}
|
|
139
|
+
MESSAGE
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if expected_failure_class && !result.error.is_a?(expected_failure_class)
|
|
143
|
+
return <<~MESSAGE
|
|
144
|
+
Incorrect instance error:
|
|
145
|
+
|
|
146
|
+
expected #{expected_failure_class}
|
|
147
|
+
got #{result.error.class.name}
|
|
148
|
+
MESSAGE
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
expected_type_value = @type_defined ? expected_type : :base
|
|
152
|
+
if result.error.type != expected_type_value
|
|
153
|
+
return <<~MESSAGE
|
|
154
|
+
Incorrect error type:
|
|
155
|
+
|
|
156
|
+
expected #{expected_type_value.inspect}
|
|
157
|
+
got #{result.error.type.inspect}
|
|
158
|
+
MESSAGE
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if @message_defined && result.error.message != expected_message
|
|
162
|
+
return <<~MESSAGE
|
|
163
|
+
Incorrect error message:
|
|
164
|
+
|
|
165
|
+
expected #{expected_message.inspect}
|
|
166
|
+
got #{result.error.message.inspect}
|
|
167
|
+
MESSAGE
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if @meta_defined && result.error.meta != expected_meta
|
|
171
|
+
return <<~MESSAGE
|
|
172
|
+
Incorrect error meta:
|
|
173
|
+
|
|
174
|
+
expected #{expected_meta.inspect}
|
|
175
|
+
got #{result.error.meta.inspect}
|
|
176
|
+
MESSAGE
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
<<~MESSAGE
|
|
180
|
+
Unexpected case when using `be_failure_service`.
|
|
181
|
+
|
|
182
|
+
Exception: #{result.error.inspect}
|
|
183
|
+
Type: #{result.error.type.inspect}
|
|
184
|
+
Message: #{result.error.message.inspect}
|
|
185
|
+
Meta: #{result.error.meta.inspect}
|
|
186
|
+
|
|
187
|
+
Please try to build an example based on the documentation.
|
|
188
|
+
Or report your problem to us:
|
|
189
|
+
|
|
190
|
+
https://github.com/servactory/servactory/issues
|
|
191
|
+
MESSAGE
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Returns the failure message for negated expectations.
|
|
195
|
+
#
|
|
196
|
+
# @return [String] Negated failure message
|
|
197
|
+
def failure_message_when_negated
|
|
198
|
+
"Expected result not to be a failed service"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Chain Methods
|
|
202
|
+
# -------------
|
|
203
|
+
|
|
204
|
+
# Specifies the expected custom failure class.
|
|
205
|
+
#
|
|
206
|
+
# Use when service is configured with a custom failure_class.
|
|
207
|
+
#
|
|
208
|
+
# @param failure_class [Class] Expected failure exception class
|
|
209
|
+
# @return [self] For method chaining
|
|
210
|
+
def with(failure_class)
|
|
211
|
+
@expected_failure_class = failure_class
|
|
212
|
+
self
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Specifies the expected error type.
|
|
216
|
+
#
|
|
217
|
+
# @param expected_type [Symbol] Expected type (defaults to :base if not set)
|
|
218
|
+
# @return [self] For method chaining
|
|
219
|
+
def type(expected_type)
|
|
220
|
+
@expected_type = expected_type
|
|
221
|
+
@type_defined = true
|
|
222
|
+
self
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Specifies the expected error message.
|
|
226
|
+
#
|
|
227
|
+
# @param expected_message [String] Expected error message text
|
|
228
|
+
# @return [self] For method chaining
|
|
229
|
+
def message(expected_message)
|
|
230
|
+
@expected_message = expected_message
|
|
231
|
+
@message_defined = true
|
|
232
|
+
self
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Specifies the expected error metadata.
|
|
236
|
+
#
|
|
237
|
+
# @param expected_meta [Hash] Expected meta hash
|
|
238
|
+
# @return [self] For method chaining
|
|
239
|
+
def meta(expected_meta)
|
|
240
|
+
@expected_meta = expected_meta
|
|
241
|
+
@meta_defined = true
|
|
242
|
+
self
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
attr_reader :result,
|
|
248
|
+
:expected_failure_class,
|
|
249
|
+
:expected_type,
|
|
250
|
+
:expected_message,
|
|
251
|
+
:expected_meta
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Result
|
|
8
|
+
# RSpec matcher for validating successful service results.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# Validates that a service result is successful and optionally contains
|
|
13
|
+
# expected output values. Used in integration tests to verify service
|
|
14
|
+
# execution completed without failure.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# RSpec.describe MyService, type: :service do
|
|
20
|
+
# it "succeeds with expected outputs" do
|
|
21
|
+
# result = described_class.call(user_id: 123)
|
|
22
|
+
#
|
|
23
|
+
# expect(result).to be_success_service
|
|
24
|
+
# .with_output(:user_name, "John")
|
|
25
|
+
# .with_output(:status, "active")
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# it "succeeds with multiple outputs" do
|
|
29
|
+
# result = described_class.call(data: input_data)
|
|
30
|
+
#
|
|
31
|
+
# expect(result).to be_success_service
|
|
32
|
+
# .with_outputs(processed: true, count: 5)
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# ```
|
|
36
|
+
#
|
|
37
|
+
# ## Chain Methods
|
|
38
|
+
#
|
|
39
|
+
# - `.with_output(key, value)` - validates single output value
|
|
40
|
+
# - `.with_outputs(hash)` - validates multiple output values at once
|
|
41
|
+
#
|
|
42
|
+
# ## Validation Steps
|
|
43
|
+
#
|
|
44
|
+
# 1. Checks result is a `Servactory::Result` instance
|
|
45
|
+
# 2. Verifies `result.success?` returns true
|
|
46
|
+
# 3. Verifies `result.failure?` returns false
|
|
47
|
+
# 4. Validates all expected outputs match actual values
|
|
48
|
+
class BeSuccessServiceMatcher
|
|
49
|
+
include RSpec::Matchers::Composable
|
|
50
|
+
|
|
51
|
+
# Creates a new success matcher with empty output expectations.
|
|
52
|
+
#
|
|
53
|
+
# @return [BeSuccessServiceMatcher] New matcher instance
|
|
54
|
+
def initialize
|
|
55
|
+
@expected_outputs = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Indicates this matcher does not support block expectations.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] Always false
|
|
61
|
+
def supports_block_expectations?
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Performs the match against the actual service result.
|
|
66
|
+
#
|
|
67
|
+
# @param result [Servactory::Result] The service result to validate
|
|
68
|
+
# @return [Boolean] True if result is successful with matching outputs
|
|
69
|
+
def matches?(result)
|
|
70
|
+
@result = result
|
|
71
|
+
|
|
72
|
+
matched = result.is_a?(Servactory::Result)
|
|
73
|
+
matched &&= result.success?
|
|
74
|
+
matched &&= !result.failure?
|
|
75
|
+
|
|
76
|
+
matched &&= expected_outputs.all? do |key, value|
|
|
77
|
+
result.respond_to?(key) && result.public_send(key) == value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
matched
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns a description of what this matcher validates.
|
|
84
|
+
#
|
|
85
|
+
# @return [String] Human-readable matcher description
|
|
86
|
+
def description
|
|
87
|
+
"service success"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns detailed failure message explaining what check failed.
|
|
91
|
+
#
|
|
92
|
+
# Checks in order: result type, success status, output existence, output values.
|
|
93
|
+
#
|
|
94
|
+
# @return [String] Detailed failure message
|
|
95
|
+
def failure_message # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
96
|
+
unless result.is_a?(Servactory::Result)
|
|
97
|
+
return <<~MESSAGE
|
|
98
|
+
Incorrect service result:
|
|
99
|
+
|
|
100
|
+
expected Servactory::Result
|
|
101
|
+
got #{result.class.name}
|
|
102
|
+
MESSAGE
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if result.failure?
|
|
106
|
+
return <<~MESSAGE
|
|
107
|
+
Incorrect service result:
|
|
108
|
+
|
|
109
|
+
expected success
|
|
110
|
+
got failure
|
|
111
|
+
MESSAGE
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
message = expected_outputs.each do |key, value|
|
|
115
|
+
unless result.respond_to?(key)
|
|
116
|
+
break <<~MESSAGE
|
|
117
|
+
Non-existent value key in result:
|
|
118
|
+
|
|
119
|
+
expected #{result.inspect}
|
|
120
|
+
got #{key}
|
|
121
|
+
MESSAGE
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
received_value = result.public_send(key)
|
|
125
|
+
next if received_value == value
|
|
126
|
+
|
|
127
|
+
break <<~MESSAGE
|
|
128
|
+
Incorrect result value for #{key}:
|
|
129
|
+
|
|
130
|
+
expected #{value.inspect}
|
|
131
|
+
got #{received_value.inspect}
|
|
132
|
+
MESSAGE
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return message if message.present? && message.is_a?(String)
|
|
136
|
+
|
|
137
|
+
<<~MESSAGE
|
|
138
|
+
Unexpected case when using `be_success_service`.
|
|
139
|
+
|
|
140
|
+
Please try to build an example based on the documentation.
|
|
141
|
+
Or report your problem to us:
|
|
142
|
+
|
|
143
|
+
https://github.com/servactory/servactory/issues
|
|
144
|
+
MESSAGE
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns the failure message for negated expectations.
|
|
148
|
+
#
|
|
149
|
+
# @return [String] Negated failure message
|
|
150
|
+
def failure_message_when_negated
|
|
151
|
+
"Expected result not to be a successful service"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Chain Methods
|
|
155
|
+
# -------------
|
|
156
|
+
|
|
157
|
+
# Specifies an expected output value on the result.
|
|
158
|
+
#
|
|
159
|
+
# @param key [Symbol] Output attribute name
|
|
160
|
+
# @param value [Object] Expected value
|
|
161
|
+
# @return [self] For method chaining
|
|
162
|
+
def with_output(key, value)
|
|
163
|
+
expected_outputs[key] = value
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Specifies multiple expected output values at once.
|
|
168
|
+
#
|
|
169
|
+
# @param attributes [Hash{Symbol => Object}] Expected output key-value pairs
|
|
170
|
+
# @return [self] For method chaining
|
|
171
|
+
def with_outputs(attributes)
|
|
172
|
+
attributes.each { |key, value| expected_outputs[key] = value }
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
attr_reader :result,
|
|
179
|
+
:expected_outputs
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|