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
data/lib/servactory/dsl.rb
CHANGED
|
@@ -16,17 +16,19 @@ module Servactory
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
::Stroma::Registry.register(:configuration, Configuration::DSL)
|
|
20
|
+
::Stroma::Registry.register(:info, Info::DSL)
|
|
21
|
+
::Stroma::Registry.register(:context, Context::DSL)
|
|
22
|
+
::Stroma::Registry.register(:inputs, Inputs::DSL)
|
|
23
|
+
::Stroma::Registry.register(:internals, Internals::DSL)
|
|
24
|
+
::Stroma::Registry.register(:outputs, Outputs::DSL)
|
|
25
|
+
::Stroma::Registry.register(:actions, Actions::DSL)
|
|
26
|
+
::Stroma::Registry.finalize!
|
|
27
|
+
|
|
19
28
|
def self.included(base)
|
|
20
|
-
base.include(
|
|
21
|
-
base.include(Info::DSL)
|
|
22
|
-
base.include(Context::DSL)
|
|
23
|
-
base.include(Inputs::DSL)
|
|
24
|
-
base.include(Internals::DSL)
|
|
25
|
-
base.include(Outputs::DSL)
|
|
29
|
+
base.include(::Stroma::DSL)
|
|
26
30
|
|
|
27
31
|
Extensions.registry.each { |extension| base.include(extension) }
|
|
28
|
-
|
|
29
|
-
base.include(Actions::DSL)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def self.with_extensions(*extensions)
|
|
@@ -74,7 +74,7 @@ module Servactory
|
|
|
74
74
|
def raise_errors
|
|
75
75
|
return if (tmp_errors = errors.not_blank).empty?
|
|
76
76
|
|
|
77
|
-
raise @context.
|
|
77
|
+
raise @context.config
|
|
78
78
|
.public_send(:"#{@attribute.system_name}_exception_class")
|
|
79
79
|
.new(context: @context, message: tmp_errors.first)
|
|
80
80
|
end
|
data/lib/servactory/result.rb
CHANGED
|
@@ -117,7 +117,7 @@ module Servactory
|
|
|
117
117
|
@outputs ||= Outputs.new(
|
|
118
118
|
outputs: @context.send(:servactory_service_warehouse).outputs,
|
|
119
119
|
predicate_methods_enabled:
|
|
120
|
-
@context.is_a?(Servactory::TestKit::Result) || @context.
|
|
120
|
+
@context.is_a?(Servactory::TestKit::Result) || @context.config.predicate_methods_enabled
|
|
121
121
|
)
|
|
122
122
|
end
|
|
123
123
|
|
|
@@ -126,7 +126,7 @@ module Servactory
|
|
|
126
126
|
def rescue_no_method_error_with(exception:) # rubocop:disable Metrics/MethodLength
|
|
127
127
|
raise exception if @context.blank? || @context.instance_of?(Servactory::TestKit::Result)
|
|
128
128
|
|
|
129
|
-
raise @context.
|
|
129
|
+
raise @context.config.failure_class.new(
|
|
130
130
|
type: :base,
|
|
131
131
|
message: @context.send(:servactory_service_info).translate(
|
|
132
132
|
"common.undefined_method.missing_name",
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module Servactory
|
|
4
4
|
module TestKit
|
|
5
|
+
# Dummy class used as a type mismatch value in tests.
|
|
6
|
+
#
|
|
7
|
+
# ## Purpose
|
|
8
|
+
#
|
|
9
|
+
# FakeType is an empty class that doesn't match any expected service
|
|
10
|
+
# input types. It's used by ValidWithSubmatcher to test type validation
|
|
11
|
+
# by providing a value guaranteed to fail type checks.
|
|
12
|
+
#
|
|
13
|
+
# ## Usage
|
|
14
|
+
#
|
|
15
|
+
# Used internally by valid_with tests:
|
|
16
|
+
#
|
|
17
|
+
# ```ruby
|
|
18
|
+
# # In valid_with submatcher
|
|
19
|
+
# prepared_attributes[attribute_name] = Servactory::TestKit::FakeType.new
|
|
20
|
+
# # This triggers type validation failure
|
|
21
|
+
# ```
|
|
22
|
+
#
|
|
23
|
+
# ## Note
|
|
24
|
+
#
|
|
25
|
+
# The class is intentionally empty - its sole purpose is to be a type
|
|
26
|
+
# that doesn't match String, Integer, Hash, Array, or any other type
|
|
27
|
+
# that a service might expect.
|
|
5
28
|
class FakeType; end # rubocop:disable Lint/EmptyClass
|
|
6
29
|
end
|
|
7
30
|
end
|
|
@@ -2,7 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module Servactory
|
|
4
4
|
module TestKit
|
|
5
|
+
# Factory for creating mock Servactory result objects.
|
|
6
|
+
#
|
|
7
|
+
# ## Purpose
|
|
8
|
+
#
|
|
9
|
+
# Provides factory methods for creating success and failure result objects
|
|
10
|
+
# in tests without actually calling a service. Used internally by the
|
|
11
|
+
# service mocking helpers to generate return values.
|
|
12
|
+
#
|
|
13
|
+
# ## Usage
|
|
14
|
+
#
|
|
15
|
+
# ```ruby
|
|
16
|
+
# # Create success result with outputs
|
|
17
|
+
# result = Servactory::TestKit::Result.as_success(
|
|
18
|
+
# service_class: MyService,
|
|
19
|
+
# user: user_object,
|
|
20
|
+
# status: :active
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# # Create failure result with exception
|
|
24
|
+
# result = Servactory::TestKit::Result.as_failure(
|
|
25
|
+
# service_class: MyService,
|
|
26
|
+
# exception: Servactory::Exceptions::Failure.new(message: "Error")
|
|
27
|
+
# )
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# ## Service Class Resolution
|
|
31
|
+
#
|
|
32
|
+
# When `service_class` is provided, uses that service's configured
|
|
33
|
+
# `result_class` for proper result type. Otherwise, uses the default
|
|
34
|
+
# `Servactory::Result` class.
|
|
5
35
|
class Result
|
|
36
|
+
# Creates a successful mock result with given outputs.
|
|
37
|
+
#
|
|
38
|
+
# @param attributes [Hash] Output attributes and optional :service_class
|
|
39
|
+
# @return [Servactory::Result] Success result with outputs as methods
|
|
6
40
|
def self.as_success(**attributes)
|
|
7
41
|
service_class = attributes.delete(:service_class) || self
|
|
8
42
|
|
|
@@ -15,6 +49,10 @@ module Servactory
|
|
|
15
49
|
end
|
|
16
50
|
end
|
|
17
51
|
|
|
52
|
+
# Creates a failed mock result with given exception.
|
|
53
|
+
#
|
|
54
|
+
# @param attributes [Hash] Output attributes, :exception, and optional :service_class
|
|
55
|
+
# @return [Servactory::Result] Failure result with error accessor
|
|
18
56
|
def self.as_failure(**attributes)
|
|
19
57
|
service_class = attributes.delete(:service_class) || self
|
|
20
58
|
exception = attributes.delete(:exception)
|
|
@@ -28,6 +66,10 @@ module Servactory
|
|
|
28
66
|
end
|
|
29
67
|
end
|
|
30
68
|
|
|
69
|
+
# Initializes result with attribute accessors.
|
|
70
|
+
#
|
|
71
|
+
# @param attributes [Hash] Output name-value pairs
|
|
72
|
+
# @return [Result] New result instance
|
|
31
73
|
def initialize(attributes = {})
|
|
32
74
|
attributes.each_pair do |name, value|
|
|
33
75
|
servactory_service_warehouse.assign_output(name, value)
|
|
@@ -38,6 +80,9 @@ module Servactory
|
|
|
38
80
|
|
|
39
81
|
private
|
|
40
82
|
|
|
83
|
+
# Internal warehouse for storing output values.
|
|
84
|
+
#
|
|
85
|
+
# @return [Servactory::Context::Warehouse::Setup] Warehouse instance
|
|
41
86
|
def servactory_service_warehouse
|
|
42
87
|
@servactory_service_warehouse ||= Servactory::Context::Warehouse::Setup.new(self)
|
|
43
88
|
end
|
|
@@ -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
|