spec_forge 0.5.0 → 0.6.0
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/.standard.yml +3 -3
- data/CHANGELOG.md +106 -1
- data/README.md +34 -22
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +91 -14
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -22
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +21 -8
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +27 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +132 -123
- data/lib/spec_forge/spec/expectation/constraint.rb +91 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
data/lib/spec_forge/cli/new.rb
CHANGED
@@ -2,6 +2,15 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Command for generating new specs or factories
|
7
|
+
#
|
8
|
+
# @example Creating a new spec
|
9
|
+
# spec_forge new spec users
|
10
|
+
#
|
11
|
+
# @example Creating a new factory
|
12
|
+
# spec_forge new factory user
|
13
|
+
#
|
5
14
|
class New < Command
|
6
15
|
command_name "new"
|
7
16
|
summary "Create a new spec or factory"
|
@@ -19,6 +28,9 @@ module SpecForge
|
|
19
28
|
|
20
29
|
aliases :generate, :g
|
21
30
|
|
31
|
+
#
|
32
|
+
# Creates a new spec or factory file in the corresponding directory using templates
|
33
|
+
#
|
22
34
|
def call
|
23
35
|
type = arguments.first.downcase
|
24
36
|
name = arguments.second
|
@@ -40,7 +52,7 @@ module SpecForge
|
|
40
52
|
def create_new_spec(name)
|
41
53
|
actions.template(
|
42
54
|
"new_spec.tt",
|
43
|
-
SpecForge.
|
55
|
+
SpecForge.forge_path.join("specs", "#{name}.yml"),
|
44
56
|
context: Proxy.new(name).call
|
45
57
|
)
|
46
58
|
end
|
@@ -48,20 +60,59 @@ module SpecForge
|
|
48
60
|
def create_new_factory(name)
|
49
61
|
actions.template(
|
50
62
|
"new_factory.tt",
|
51
|
-
SpecForge.
|
63
|
+
SpecForge.forge_path.join("factories", "#{name}.yml"),
|
52
64
|
context: Proxy.new(name).call
|
53
65
|
)
|
54
66
|
end
|
55
67
|
|
68
|
+
#
|
69
|
+
# Helper class for passing template variables to Thor templates
|
70
|
+
#
|
71
|
+
# @example Creating a proxy with a name
|
72
|
+
# proxy = Proxy.new("user")
|
73
|
+
# proxy.singular_name # => "user"
|
74
|
+
# proxy.plural_name # => "users"
|
75
|
+
#
|
56
76
|
class Proxy
|
57
|
-
|
77
|
+
#
|
78
|
+
# The original name passed to the command
|
79
|
+
#
|
80
|
+
# @return [String]
|
81
|
+
#
|
82
|
+
attr_reader :original_name
|
58
83
|
|
84
|
+
#
|
85
|
+
# The singular form of the name
|
86
|
+
#
|
87
|
+
# @return [String]
|
88
|
+
#
|
89
|
+
attr_reader :singular_name
|
90
|
+
|
91
|
+
#
|
92
|
+
# The plural form of the name
|
93
|
+
#
|
94
|
+
# @return [String]
|
95
|
+
#
|
96
|
+
attr_reader :plural_name
|
97
|
+
|
98
|
+
#
|
99
|
+
# Creates a new Proxy with the specified name
|
100
|
+
#
|
101
|
+
# @param name [String] The resource name to pluralize/singularize
|
102
|
+
#
|
103
|
+
# @return [Proxy] A new proxy instance
|
104
|
+
#
|
59
105
|
def initialize(name)
|
60
106
|
@original_name = name
|
61
107
|
@plural_name = name.pluralize
|
62
108
|
@singular_name = name.singularize
|
63
109
|
end
|
64
110
|
|
111
|
+
#
|
112
|
+
# Returns a binding for use in templates
|
113
|
+
#
|
114
|
+
# @return [Binding] A binding containing template variables
|
115
|
+
#
|
65
116
|
def call
|
66
117
|
binding
|
67
118
|
end
|
data/lib/spec_forge/cli/run.rb
CHANGED
@@ -2,6 +2,21 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Command for running SpecForge tests with filtering options
|
7
|
+
#
|
8
|
+
# @example Running all specs
|
9
|
+
# spec_forge run
|
10
|
+
#
|
11
|
+
# @example Running specific file
|
12
|
+
# spec_forge run users
|
13
|
+
#
|
14
|
+
# @example Running specific spec
|
15
|
+
# spec_forge run users:create_user
|
16
|
+
#
|
17
|
+
# @example Running specific expectation
|
18
|
+
# spec_forge run users:create_user:"POST /users"
|
19
|
+
#
|
5
20
|
class Run < Command
|
6
21
|
command_name "run"
|
7
22
|
syntax "run [target]"
|
@@ -26,6 +41,9 @@ module SpecForge
|
|
26
41
|
|
27
42
|
# option "-n", "--no-docs", "Do not generate OpenAPI documentation on completion"
|
28
43
|
|
44
|
+
#
|
45
|
+
# Loads and runs all specs, or a subset of specs based on the provided arguments
|
46
|
+
#
|
29
47
|
def call
|
30
48
|
return SpecForge.run if arguments.blank?
|
31
49
|
|
@@ -53,6 +71,8 @@ module SpecForge
|
|
53
71
|
# Example with name:
|
54
72
|
# "users:show_user:'GET /users/:id - Returns 404 due to missing user'"
|
55
73
|
#
|
74
|
+
# @private
|
75
|
+
#
|
56
76
|
def extract_filter(input)
|
57
77
|
# Note: Only split 3 because the expectation name can have colons in them.
|
58
78
|
file_name, spec_name, expectation_name = input.split(":", 3).map(&:strip)
|
data/lib/spec_forge/cli.rb
CHANGED
@@ -7,15 +7,26 @@ require_relative "cli/new"
|
|
7
7
|
require_relative "cli/run"
|
8
8
|
|
9
9
|
module SpecForge
|
10
|
+
#
|
11
|
+
# Command-line interface for SpecForge that provides the overall command structure
|
12
|
+
# and entry point for the CLI functionality.
|
13
|
+
#
|
14
|
+
# @example Running the default command
|
15
|
+
# SpecForge::CLI.new.run
|
16
|
+
#
|
17
|
+
# @example Running a specific command
|
18
|
+
# # From command line: spec_forge init
|
19
|
+
#
|
10
20
|
class CLI
|
11
21
|
include Commander::Methods
|
12
22
|
|
13
|
-
COMMANDS = [Init, New, Run]
|
14
|
-
|
15
23
|
#
|
16
|
-
#
|
24
|
+
# @return [Array<SpecForge::CLI::Command>] All available commands
|
17
25
|
#
|
18
|
-
|
26
|
+
COMMANDS = [Init, New, Run].freeze
|
27
|
+
|
28
|
+
#
|
29
|
+
# Runs the CLI application, setting up program information and registering commands
|
19
30
|
#
|
20
31
|
def run
|
21
32
|
program :name, "SpecForge"
|
@@ -30,7 +41,7 @@ module SpecForge
|
|
30
41
|
end
|
31
42
|
|
32
43
|
#
|
33
|
-
# Registers the
|
44
|
+
# Registers the command classes with Commander
|
34
45
|
#
|
35
46
|
# @private
|
36
47
|
#
|
@@ -1,43 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
#
|
5
|
+
# Configuration container for SpecForge settings
|
6
|
+
# Defines default values and validation for all configuration options
|
7
|
+
#
|
8
|
+
class Configuration < Struct.new(:base_url, :headers, :query, :factories, :on_debug)
|
9
|
+
#
|
10
|
+
# Manages factory configuration settings
|
11
|
+
# Controls auto-discovery behavior and custom factory paths
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# config.factories.auto_discover = false
|
15
|
+
# config.factories.paths += ["lib/factories"]
|
16
|
+
#
|
7
17
|
class Factories < Struct.new(:auto_discover, :paths)
|
18
|
+
#
|
19
|
+
# Creates reader methods that return boolean values
|
20
|
+
# Allows for checking configuration with predicate methods
|
21
|
+
#
|
8
22
|
attr_predicate :auto_discover, :paths
|
9
23
|
|
24
|
+
#
|
25
|
+
# Initializes a new Factories configuration
|
26
|
+
# Sets default values for auto-discovery and paths
|
27
|
+
#
|
28
|
+
# @param auto_discover [Boolean] Whether to auto-discover factories (default: true)
|
29
|
+
# @param paths [Array<String>] Additional paths to look for factories (default: [])
|
30
|
+
#
|
31
|
+
# @return [Factories] A new factories configuration instance
|
32
|
+
#
|
10
33
|
def initialize(auto_discover: true, paths: []) = super
|
11
34
|
end
|
12
35
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
overlay_value
|
20
|
-
# If source is nil and overlay exists (but wasn't "present"), use overlay
|
21
|
-
elsif source_value.nil? && !overlay_value.nil?
|
22
|
-
overlay_value
|
23
|
-
# Otherwise keep source value
|
24
|
-
else
|
25
|
-
source_value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
36
|
+
#
|
37
|
+
# Initializes a new Configuration with default values
|
38
|
+
# Sets up the configuration structure including factory settings and debug proxy
|
39
|
+
#
|
40
|
+
# @return [Configuration] A new configuration instance with defaults
|
41
|
+
#
|
30
42
|
def initialize
|
31
43
|
config = Normalizer.default_configuration
|
32
44
|
|
33
45
|
config[:base_url] = "http://localhost:3000"
|
34
46
|
config[:factories] = Factories.new
|
35
|
-
config[:specs] = RSpec.configuration
|
36
47
|
config[:on_debug] = Runner::DebugProxy.default
|
37
48
|
|
38
49
|
super(**config)
|
39
50
|
end
|
40
51
|
|
52
|
+
#
|
53
|
+
# Validates the configuration and applies normalization
|
54
|
+
# Ensures all required fields have values and applies defaults when needed
|
55
|
+
#
|
56
|
+
# @return [self] Returns self for method chaining
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
#
|
41
60
|
def validate
|
42
61
|
output = Normalizer.normalize_configuration!(to_h)
|
43
62
|
|
@@ -49,10 +68,63 @@ module SpecForge
|
|
49
68
|
self
|
50
69
|
end
|
51
70
|
|
71
|
+
#
|
72
|
+
# Recursively converts the configuration to a hash representation
|
73
|
+
#
|
74
|
+
# @return [Hash] Hash representation of the configuration
|
75
|
+
#
|
52
76
|
def to_h
|
53
|
-
hash = super
|
77
|
+
hash = super
|
54
78
|
hash[:factories] = hash[:factories].to_h
|
55
79
|
hash
|
56
80
|
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Returns the RSpec configuration object
|
84
|
+
# Provides access to RSpec's internal configuration for test customization
|
85
|
+
#
|
86
|
+
# @return [RSpec::Core::Configuration] RSpec's configuration object
|
87
|
+
#
|
88
|
+
# @example Setting formatter options
|
89
|
+
# SpecForge.configure do |config|
|
90
|
+
# config.specs.formatter = :documentation
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
def specs
|
94
|
+
RSpec.configuration
|
95
|
+
end
|
96
|
+
|
97
|
+
alias_method :rspec, :specs
|
98
|
+
|
99
|
+
#
|
100
|
+
# Registers a callback for a specific test lifecycle event
|
101
|
+
# Allows custom code execution at specific points during test execution
|
102
|
+
#
|
103
|
+
# @param name [Symbol, String] The callback point to register for
|
104
|
+
# (:before_file, :after_expectation, etc.)
|
105
|
+
# @yield A block to execute when the callback is triggered
|
106
|
+
# @yieldparam context [Object] An object containing context-specific state data, depending
|
107
|
+
# on which hook the callback is triggered from.
|
108
|
+
#
|
109
|
+
# @return [Proc] The registered callback
|
110
|
+
#
|
111
|
+
# @example Registering a custom debug handler
|
112
|
+
# SpecForge.configure do |config|
|
113
|
+
# config.register_callback(:on_debug) { binding.pry }
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# @example Cleaning database after each test
|
117
|
+
# SpecForge.configure do |config|
|
118
|
+
# config.register_callback(:after_expectation) do
|
119
|
+
# DatabaseCleaner.clean
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
def register_callback(name, &)
|
124
|
+
Callbacks.register(name, &)
|
125
|
+
end
|
126
|
+
|
127
|
+
alias_method :define_callback, :register_callback
|
128
|
+
alias_method :callback, :register_callback
|
57
129
|
end
|
58
130
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages user-defined callbacks grouped by lifecycle hook
|
7
|
+
#
|
8
|
+
# This class collects and organizes callbacks by their hook type
|
9
|
+
# (before_file, after_each, etc.) to support the test lifecycle.
|
10
|
+
# It ensures callbacks are properly categorized for execution.
|
11
|
+
#
|
12
|
+
# @example Creating callback groups
|
13
|
+
# callbacks = Context::Callbacks.new([
|
14
|
+
# {before_file: "setup_environment"},
|
15
|
+
# {after_each: "log_test_result"}
|
16
|
+
# ])
|
17
|
+
#
|
18
|
+
class Callbacks
|
19
|
+
#
|
20
|
+
# Creates a new callbacks collection
|
21
|
+
#
|
22
|
+
# @param callback_array [Array] Optional initial callbacks to register
|
23
|
+
#
|
24
|
+
# @return [Callbacks] A new callbacks collection
|
25
|
+
#
|
26
|
+
def initialize(callback_array = [])
|
27
|
+
set(callback_array)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Updates the callbacks collection
|
32
|
+
#
|
33
|
+
# @param callback_array [Array] New callbacks to register
|
34
|
+
#
|
35
|
+
# @return [self]
|
36
|
+
#
|
37
|
+
def set(callback_array)
|
38
|
+
@inner = organize_callbacks_by_hook(callback_array)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Returns the hash representation of callbacks
|
44
|
+
#
|
45
|
+
# @return [Hash] Callbacks organized by hook type
|
46
|
+
#
|
47
|
+
def to_h
|
48
|
+
@inner
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Executes all registered callbacks for a specific lifecycle hook
|
53
|
+
#
|
54
|
+
# @param hook_name [String, Symbol] The lifecycle hook (before_file, after_each, etc.)
|
55
|
+
# @param context [Hash] State data that will be converted to a structured object
|
56
|
+
# and passed to callbacks
|
57
|
+
#
|
58
|
+
def run(hook_name, context = {})
|
59
|
+
context = context.to_struct
|
60
|
+
|
61
|
+
@inner[hook_name].each do |callback_name|
|
62
|
+
SpecForge::Callbacks.run(callback_name, context)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
#
|
69
|
+
# Organizes callbacks from an array to hash structure by hook type
|
70
|
+
# Groups callbacks like before_file, after_each, etc. for easier lookup
|
71
|
+
#
|
72
|
+
# @param callback_array [Array] The array of callbacks
|
73
|
+
#
|
74
|
+
# @return [Hash] Callbacks indexed by hook type
|
75
|
+
#
|
76
|
+
# @private
|
77
|
+
#
|
78
|
+
def organize_callbacks_by_hook(callback_array)
|
79
|
+
groups = Hash.new { |h, k| h[k] = Set.new }
|
80
|
+
|
81
|
+
callback_array.each_with_object(groups) do |callbacks, groups|
|
82
|
+
callbacks.each do |hook, name|
|
83
|
+
next if name.blank?
|
84
|
+
|
85
|
+
groups[hook].add(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages global state and variables at the spec file level.
|
7
|
+
#
|
8
|
+
# The Global class provides access to variables that are defined at the global level
|
9
|
+
# in a spec file and are accessible across all specs and expectations in a file.
|
10
|
+
# Unlike regular variables, global variables do not support overlaying - they maintain
|
11
|
+
# consistent values throughout test execution.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# global = Global.new(variables: {api_version: "v2", environment: "test"})
|
15
|
+
#
|
16
|
+
# global.variables[:api_version] #=> "v2"
|
17
|
+
# global.variables[:environment] #=> "test"
|
18
|
+
#
|
19
|
+
# # Update global variables
|
20
|
+
# global.set(variables: {environment: "staging"})
|
21
|
+
# global.variables[:environment] #=> "staging"
|
22
|
+
# global.variables[:api_version] #=> nil
|
23
|
+
#
|
24
|
+
class Global
|
25
|
+
# @return [Context::Variables] The container for global variables
|
26
|
+
attr_reader :variables
|
27
|
+
|
28
|
+
# @return [Context::Callbacks] The container for callbacks
|
29
|
+
attr_reader :callbacks
|
30
|
+
|
31
|
+
#
|
32
|
+
# Creates a new Global context instance
|
33
|
+
#
|
34
|
+
# @param variables [Hash<Symbol, Object>] A hash of variable names and values
|
35
|
+
# @param callbacks [Array<Hash<Symbol, String>>] An array of callback hooks
|
36
|
+
#
|
37
|
+
# @return [Global] The new Global instance
|
38
|
+
#
|
39
|
+
def initialize(variables: {}, callbacks: [])
|
40
|
+
@variables = Variables.new(base: variables)
|
41
|
+
@callbacks = Callbacks.new(callbacks)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Sets the global variables
|
46
|
+
#
|
47
|
+
# @param variables [Hash<Symbol, Object>] A hash of variable names and values
|
48
|
+
# @param callbacks [Array<Hash<Symbol, String>>] An array of callback hooks
|
49
|
+
#
|
50
|
+
# @return [self]
|
51
|
+
#
|
52
|
+
def set(variables: {}, callbacks: [])
|
53
|
+
@variables.set(base: variables)
|
54
|
+
@callbacks.set(callbacks)
|
55
|
+
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Returns a hash representation of the global context
|
61
|
+
#
|
62
|
+
# @return [Hash]
|
63
|
+
#
|
64
|
+
def to_h
|
65
|
+
{
|
66
|
+
variables: variables.to_h,
|
67
|
+
callbacks: callbacks.to_h
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages storage of API responses for use in subsequent tests
|
7
|
+
#
|
8
|
+
# This class provides a mechanism to store HTTP requests and responses
|
9
|
+
# during test execution, allowing values to be referenced in later tests
|
10
|
+
# through the `store.id.body.attribute` syntax.
|
11
|
+
#
|
12
|
+
# @example Storing and retrieving a response in specs
|
13
|
+
# # In one expectation:
|
14
|
+
# store_as: user_creation
|
15
|
+
#
|
16
|
+
# # In a later test:
|
17
|
+
# query:
|
18
|
+
# id: store.user_creation.body.id
|
19
|
+
#
|
20
|
+
class Store
|
21
|
+
#
|
22
|
+
# Represents a single stored entry with request, variables, and response data
|
23
|
+
#
|
24
|
+
# Entries are immutable once created and contain a deep-frozen
|
25
|
+
# snapshot of the test state at the time of storage.
|
26
|
+
#
|
27
|
+
# @example Accessing stored entry data
|
28
|
+
# entry = store["user_creation"]
|
29
|
+
# entry.status # => 201
|
30
|
+
# entry.body.id # => 42
|
31
|
+
#
|
32
|
+
class Entry < Data.define(:scope, :request, :variables, :response)
|
33
|
+
#
|
34
|
+
# Creates a new immutable store entry
|
35
|
+
#
|
36
|
+
# @param request [Hash] The HTTP request that was executed
|
37
|
+
# @param variables [Hash] Variables from the test context
|
38
|
+
# @param response [Hash] The HTTP response received
|
39
|
+
# @param scope [Symbol] Scope of this entry, either :file or :spec
|
40
|
+
#
|
41
|
+
# @return [Entry] A new immutable entry instance
|
42
|
+
#
|
43
|
+
def initialize(request:, variables:, response:, scope: :file)
|
44
|
+
request = request.deep_freeze
|
45
|
+
variables = variables.deep_freeze
|
46
|
+
response = response.deep_freeze
|
47
|
+
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Shorthand accessor for the HTTP status code
|
53
|
+
#
|
54
|
+
# @return [Integer] The response status code
|
55
|
+
#
|
56
|
+
def status = response[:status]
|
57
|
+
|
58
|
+
#
|
59
|
+
# Shorthand accessor for the response body
|
60
|
+
#
|
61
|
+
# @return [Hash, Array, String] The parsed response body
|
62
|
+
#
|
63
|
+
def body = response[:body]
|
64
|
+
|
65
|
+
#
|
66
|
+
# Shorthand accessor for the response headers
|
67
|
+
#
|
68
|
+
# @return [Hash] The response headers
|
69
|
+
#
|
70
|
+
def headers = response[:headers]
|
71
|
+
|
72
|
+
#
|
73
|
+
# Returns all available methods that can be called
|
74
|
+
#
|
75
|
+
# @return [Array] The method names
|
76
|
+
#
|
77
|
+
def available_methods
|
78
|
+
members + [:status, :body, :headers]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Creates a new empty store
|
84
|
+
#
|
85
|
+
# @return [Store] A new store instance
|
86
|
+
#
|
87
|
+
def initialize
|
88
|
+
@inner = {}
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Retrieves a stored entry by ID
|
93
|
+
#
|
94
|
+
# @param id [String, Symbol] The identifier for the stored entry
|
95
|
+
#
|
96
|
+
# @return [Entry, nil] The stored entry or nil if not found
|
97
|
+
#
|
98
|
+
def [](id)
|
99
|
+
@inner[id]
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Returns the number of entries in the store
|
104
|
+
#
|
105
|
+
# @return [Integer] The count of stored entries
|
106
|
+
#
|
107
|
+
def size
|
108
|
+
@inner.size
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Stores an entry with the specified ID
|
113
|
+
#
|
114
|
+
# @param id [String, Symbol] The identifier to store the entry under
|
115
|
+
#
|
116
|
+
# @return [self]
|
117
|
+
#
|
118
|
+
def set(id, **)
|
119
|
+
@inner[id] = Entry.new(**)
|
120
|
+
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Removes all entries from the store
|
126
|
+
#
|
127
|
+
def clear
|
128
|
+
@inner.clear
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Removes all spec entries from the store
|
133
|
+
#
|
134
|
+
def clear_specs
|
135
|
+
@inner.delete_if { |_k, v| v.scope == :spec }
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# Returns a hash representation of store
|
140
|
+
#
|
141
|
+
# @return [Hash]
|
142
|
+
#
|
143
|
+
def to_h
|
144
|
+
@inner.transform_values(&:to_h).deep_stringify_keys
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|