http_decoy 0.1.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 +7 -0
- data/.rubocop.yml +102 -0
- data/CHANGELOG.md +39 -0
- data/CONTRIBUTING.md +67 -0
- data/LICENSE +21 -0
- data/README.md +478 -0
- data/Rakefile +8 -0
- data/lib/httpfake/configuration.rb +11 -0
- data/lib/httpfake/handler_context.rb +109 -0
- data/lib/httpfake/request_log.rb +44 -0
- data/lib/httpfake/route.rb +15 -0
- data/lib/httpfake/route_map.rb +36 -0
- data/lib/httpfake/router.rb +43 -0
- data/lib/httpfake/rspec.rb +206 -0
- data/lib/httpfake/server.rb +166 -0
- data/lib/httpfake/version.rb +5 -0
- data/lib/httpfake/webmock_integration.rb +43 -0
- data/lib/httpfake.rb +48 -0
- data/sig/httpfake.rbs +4 -0
- metadata +96 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HttpFake
|
|
4
|
+
# Matches an incoming (method, path) pair against a list of Route objects.
|
|
5
|
+
# Supports :param segments in patterns, e.g. "/v1/charges/:id".
|
|
6
|
+
class Router
|
|
7
|
+
Match = Struct.new(:route, :params, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
def initialize(routes)
|
|
10
|
+
@routes = routes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns a Match or nil.
|
|
14
|
+
def match(method, path, scenario: nil)
|
|
15
|
+
http_method = method.to_s.upcase
|
|
16
|
+
@routes.each do |route|
|
|
17
|
+
next unless route.method == http_method
|
|
18
|
+
next unless route.scenario == scenario
|
|
19
|
+
|
|
20
|
+
captures = extract_params(route.pattern, path)
|
|
21
|
+
next if captures.nil?
|
|
22
|
+
|
|
23
|
+
return Match.new(route: route, params: captures)
|
|
24
|
+
end
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def extract_params(pattern, path)
|
|
31
|
+
regex = pattern_to_regex(pattern)
|
|
32
|
+
m = path.match(regex)
|
|
33
|
+
return nil unless m
|
|
34
|
+
|
|
35
|
+
m.named_captures.transform_keys(&:to_sym)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pattern_to_regex(pattern)
|
|
39
|
+
escaped = pattern.gsub(/:([a-zA-Z_][a-zA-Z0-9_]*)/) { "(?<#{Regexp.last_match(1)}>[^/]+)" }
|
|
40
|
+
/\A#{escaped}\z/
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
require_relative "route_map"
|
|
5
|
+
require_relative "server"
|
|
6
|
+
require_relative "webmock_integration"
|
|
7
|
+
|
|
8
|
+
module HttpFake
|
|
9
|
+
# RSpec integration.
|
|
10
|
+
#
|
|
11
|
+
# Two equivalent usage patterns:
|
|
12
|
+
#
|
|
13
|
+
# Pattern A — inline (per describe block):
|
|
14
|
+
#
|
|
15
|
+
# RSpec.describe MyService do
|
|
16
|
+
# include HttpFake::RSpec
|
|
17
|
+
#
|
|
18
|
+
# fake_server(:payments) do
|
|
19
|
+
# post "/charges" do
|
|
20
|
+
# respond 201, json: { id: "ch_abc" }
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# it "creates a charge" do
|
|
25
|
+
# MyService.charge(100)
|
|
26
|
+
# expect(fake_server(:payments)).to have_received_request(:post, "/charges").once
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# Pattern B — suite-wide definition (most common):
|
|
31
|
+
#
|
|
32
|
+
# FakeStripe = HttpFake.define(:stripe) do
|
|
33
|
+
# base_url "https://api.stripe.com"
|
|
34
|
+
# post "/v1/charges" do
|
|
35
|
+
# respond 200, json: { id: "ch_123" }
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# RSpec.configure { |c| c.include FakeStripe.rspec_helpers }
|
|
40
|
+
#
|
|
41
|
+
# it "charges the card" do
|
|
42
|
+
# StripeService.charge(500)
|
|
43
|
+
# expect(fake_server(:stripe)).to have_received_request(:post, "/v1/charges").once
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
module RSpec
|
|
47
|
+
def self.included(base)
|
|
48
|
+
base.extend(ClassMethods)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module ClassMethods
|
|
52
|
+
# Class-level macro. Evaluates the block into a RouteMap once at class-load
|
|
53
|
+
# time, then registers before/after hooks for per-example server lifecycle.
|
|
54
|
+
def fake_server(name, &)
|
|
55
|
+
route_map = RouteMap.new
|
|
56
|
+
route_map.instance_eval(&)
|
|
57
|
+
_httpfake_register(name, route_map)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Internal: register before/after hooks for a pre-built RouteMap.
|
|
61
|
+
# Called by both the inline macro and Definition#rspec_helpers.
|
|
62
|
+
def _httpfake_register(name, route_map)
|
|
63
|
+
before(:each) do
|
|
64
|
+
server = Server.new(route_map)
|
|
65
|
+
server.start
|
|
66
|
+
stub = WebMockIntegration.setup(server)
|
|
67
|
+
|
|
68
|
+
@_httpfake_servers ||= {}
|
|
69
|
+
@_httpfake_webmock_stubs ||= {}
|
|
70
|
+
@_httpfake_servers[name] = server
|
|
71
|
+
@_httpfake_webmock_stubs[name] = stub
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
after(:each) do
|
|
75
|
+
server = @_httpfake_servers&.[](name)
|
|
76
|
+
stub = @_httpfake_webmock_stubs&.[](name)
|
|
77
|
+
WebMockIntegration.teardown(stub)
|
|
78
|
+
server&.stop
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Instance-level accessor — returns the live Server for this example.
|
|
84
|
+
def fake_server(name)
|
|
85
|
+
@_httpfake_servers[name]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Run a block with a named scenario active.
|
|
89
|
+
# server_name defaults to the only server if exactly one is registered.
|
|
90
|
+
def with_scenario(scenario_name, server_name = nil, &)
|
|
91
|
+
name = server_name || begin
|
|
92
|
+
servers = @_httpfake_servers || {}
|
|
93
|
+
raise ArgumentError, "server_name required when multiple fake servers are active" if servers.size > 1
|
|
94
|
+
raise ArgumentError, "No fake servers are active" if servers.empty?
|
|
95
|
+
|
|
96
|
+
servers.keys.first
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
server = @_httpfake_servers[name]
|
|
100
|
+
raise ArgumentError, "No fake server named #{name.inspect}" unless server
|
|
101
|
+
|
|
102
|
+
server.with_scenario(scenario_name, &)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# RSpec matchers
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
::RSpec::Matchers.define :have_received_request do |method, path|
|
|
111
|
+
match do |server|
|
|
112
|
+
entries = server.request_log.for(method, path)
|
|
113
|
+
next false if entries.empty?
|
|
114
|
+
next false if @times && entries.count != @times
|
|
115
|
+
next false if @body_matcher && entries.none? { |e| body_matches?(e.body, @body_matcher) }
|
|
116
|
+
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
chain :once do
|
|
121
|
+
@times = 1
|
|
122
|
+
end
|
|
123
|
+
chain :twice do
|
|
124
|
+
@times = 2
|
|
125
|
+
end
|
|
126
|
+
chain :times do |n|
|
|
127
|
+
@times = n
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
chain :with do |body: nil|
|
|
131
|
+
@body_matcher = body
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
failure_message do |server|
|
|
135
|
+
entries = server.request_log.for(method, path)
|
|
136
|
+
if entries.empty?
|
|
137
|
+
"expected #{method.to_s.upcase} #{path} to have been received, but it was never called"
|
|
138
|
+
elsif @times && entries.count != @times
|
|
139
|
+
"expected #{method.to_s.upcase} #{path} to have been received #{@times} time(s), " \
|
|
140
|
+
"but it was received #{entries.count} time(s)"
|
|
141
|
+
else
|
|
142
|
+
"expected #{method.to_s.upcase} #{path} body to match #{@body_matcher.inspect}, " \
|
|
143
|
+
"but received: #{entries.map(&:body).inspect}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
failure_message_when_negated do |_server|
|
|
148
|
+
"expected #{method.to_s.upcase} #{path} not to have been received"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
description do
|
|
152
|
+
desc = "have received #{method.to_s.upcase} #{path}"
|
|
153
|
+
desc += " #{@times} time(s)" if @times
|
|
154
|
+
desc += " with body matching #{@body_matcher.inspect}" if @body_matcher
|
|
155
|
+
desc
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def body_matches?(actual, matcher)
|
|
159
|
+
case matcher
|
|
160
|
+
when Hash
|
|
161
|
+
actual.is_a?(Hash) && matcher.all? do |k, v|
|
|
162
|
+
actual_val = actual[k] || actual[k.to_s]
|
|
163
|
+
v === actual_val
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
matcher === actual
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Definition — returned by HttpFake.define
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
# Wraps a named RouteMap and generates an anonymous RSpec helper module.
|
|
176
|
+
class Definition
|
|
177
|
+
attr_reader :name, :route_map
|
|
178
|
+
|
|
179
|
+
def initialize(name, route_map)
|
|
180
|
+
@name = name
|
|
181
|
+
@route_map = route_map
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns an anonymous module. Include it in RSpec.configure to register
|
|
185
|
+
# the server lifecycle for every example group in the suite.
|
|
186
|
+
#
|
|
187
|
+
# RSpec.configure { |c| c.include FakeStripe.rspec_helpers }
|
|
188
|
+
#
|
|
189
|
+
def rspec_helpers
|
|
190
|
+
definition = self
|
|
191
|
+
|
|
192
|
+
Module.new do
|
|
193
|
+
include HttpFake::RSpec
|
|
194
|
+
|
|
195
|
+
# define_singleton_method closes over `definition` from the outer scope.
|
|
196
|
+
# `def self.included` would NOT — def never captures outer locals.
|
|
197
|
+
define_singleton_method(:included) do |base|
|
|
198
|
+
super(base)
|
|
199
|
+
base._httpfake_register(definition.name, definition.route_map)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
define_method(:_httpfake_definition) { definition }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webrick"
|
|
4
|
+
require "rack"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "json"
|
|
7
|
+
require_relative "request_log"
|
|
8
|
+
require_relative "handler_context"
|
|
9
|
+
|
|
10
|
+
module HttpFake
|
|
11
|
+
# A real WEBrick HTTP server that runs in a background thread.
|
|
12
|
+
#
|
|
13
|
+
# Uses WEBrick directly (no Rack::Handler) so it works with both
|
|
14
|
+
# Rack 2.x and Rack 3.x without the rackup gem.
|
|
15
|
+
#
|
|
16
|
+
# Port 0 lets the OS pick a free port atomically — parallel test
|
|
17
|
+
# runners never collide.
|
|
18
|
+
class Server
|
|
19
|
+
attr_reader :route_map, :request_log, :port
|
|
20
|
+
|
|
21
|
+
def initialize(route_map)
|
|
22
|
+
@route_map = route_map
|
|
23
|
+
@request_log = RequestLog.new
|
|
24
|
+
@webrick = nil
|
|
25
|
+
@thread = nil
|
|
26
|
+
@port = nil
|
|
27
|
+
@scenario = nil
|
|
28
|
+
@scenario_mu = Mutex.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def start
|
|
32
|
+
rack_app = build_rack_app
|
|
33
|
+
|
|
34
|
+
# WEBrick binds to the OS-assigned port during initialization.
|
|
35
|
+
@webrick = WEBrick::HTTPServer.new(
|
|
36
|
+
Port: 0,
|
|
37
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
38
|
+
AccessLog: []
|
|
39
|
+
)
|
|
40
|
+
@port = @webrick.config[:Port]
|
|
41
|
+
|
|
42
|
+
@webrick.mount_proc("/") do |req, res|
|
|
43
|
+
status, headers, body = rack_app.call(rack_env_from(req))
|
|
44
|
+
res.status = status.to_i
|
|
45
|
+
headers.each { |k, v| res[k] = v }
|
|
46
|
+
res.body = Array(body).join
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
res.status = 500
|
|
49
|
+
res["Content-Type"] = "application/json"
|
|
50
|
+
res.body = JSON.generate(error: "#{e.class}: #{e.message}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@thread = Thread.new { @webrick.start }
|
|
54
|
+
@thread.abort_on_exception = true
|
|
55
|
+
|
|
56
|
+
# Poll until WEBrick enters its accept loop.
|
|
57
|
+
deadline = Time.now + 5
|
|
58
|
+
sleep(0.005) until @webrick.status == :Running || Time.now > deadline
|
|
59
|
+
raise "httpfake: server failed to start within 5 seconds" unless @webrick.status == :Running
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stop
|
|
65
|
+
@webrick&.shutdown
|
|
66
|
+
@thread&.join(3)
|
|
67
|
+
@thread = nil
|
|
68
|
+
@webrick = nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def base_url = "http://127.0.0.1:#{@port}"
|
|
72
|
+
|
|
73
|
+
# The Rack app is also exposed for WebMock's to_rack() interception.
|
|
74
|
+
def rack_app
|
|
75
|
+
@rack_app ||= build_rack_app
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def with_scenario(name)
|
|
79
|
+
@scenario_mu.synchronize { @scenario = name }
|
|
80
|
+
yield
|
|
81
|
+
ensure
|
|
82
|
+
@scenario_mu.synchronize { @scenario = nil }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def current_scenario
|
|
86
|
+
@scenario_mu.synchronize { @scenario }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def build_rack_app
|
|
92
|
+
route_map = @route_map
|
|
93
|
+
request_log = @request_log
|
|
94
|
+
server = self
|
|
95
|
+
|
|
96
|
+
lambda do |env|
|
|
97
|
+
req = Rack::Request.new(env)
|
|
98
|
+
method = req.request_method
|
|
99
|
+
path = req.path_info
|
|
100
|
+
scenario = server.current_scenario
|
|
101
|
+
|
|
102
|
+
result = route_map.router.match(method, path, scenario: scenario)
|
|
103
|
+
result ||= route_map.router.match(method, path, scenario: nil) if scenario
|
|
104
|
+
|
|
105
|
+
return json_response(404, error: "No route matches #{method} #{path}") unless result
|
|
106
|
+
|
|
107
|
+
call_index = request_log.for(method, path).count
|
|
108
|
+
ctx = HandlerContext.new(req, result.params, call_index: call_index)
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
ctx.instance_eval(&result.route.handler_block)
|
|
112
|
+
rescue HandlerContext::ContractError => e
|
|
113
|
+
return json_response(422, error: e.message)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
request_log.record(
|
|
117
|
+
method: method,
|
|
118
|
+
path: path,
|
|
119
|
+
body: ctx.body,
|
|
120
|
+
headers: env.select { |k, _| k.start_with?("HTTP_") },
|
|
121
|
+
query_params: ctx.query_params
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
ctx.response || json_response(200, status: "ok")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Convert a WEBrick::HTTPRequest into a Rack-compatible env hash.
|
|
129
|
+
# Uses req.header (a Hash with lowercase keys) rather than each_header,
|
|
130
|
+
# which does not exist on WEBrick::HTTPRequest.
|
|
131
|
+
def rack_env_from(req)
|
|
132
|
+
body_str = req.body || ""
|
|
133
|
+
|
|
134
|
+
env = {
|
|
135
|
+
"REQUEST_METHOD" => req.request_method,
|
|
136
|
+
"SCRIPT_NAME" => "",
|
|
137
|
+
"PATH_INFO" => req.path,
|
|
138
|
+
"QUERY_STRING" => req.query_string || "",
|
|
139
|
+
"SERVER_NAME" => "127.0.0.1",
|
|
140
|
+
"SERVER_PORT" => @port.to_s,
|
|
141
|
+
"CONTENT_TYPE" => req.content_type || "",
|
|
142
|
+
"CONTENT_LENGTH" => body_str.bytesize.to_s,
|
|
143
|
+
"rack.input" => StringIO.new(body_str),
|
|
144
|
+
"rack.errors" => $stderr,
|
|
145
|
+
"rack.multithread" => true,
|
|
146
|
+
"rack.multiprocess" => false,
|
|
147
|
+
"rack.run_once" => false,
|
|
148
|
+
"rack.url_scheme" => "http"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# req.header is a Hash of { "lowercase-name" => ["value"] }
|
|
152
|
+
req.header.each do |key, values|
|
|
153
|
+
rack_key = key.upcase.tr("-", "_")
|
|
154
|
+
next if %w[CONTENT_TYPE CONTENT_LENGTH].include?(rack_key)
|
|
155
|
+
|
|
156
|
+
env["HTTP_#{rack_key}"] = Array(values).join(", ")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
env
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def json_response(status, payload)
|
|
163
|
+
[status.to_i, { "Content-Type" => "application/json" }, [JSON.generate(payload)]]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HttpFake
|
|
4
|
+
# Manages the WebMock stub that routes a declared base_url to the server's Rack app.
|
|
5
|
+
#
|
|
6
|
+
# Auto-detect: if WebMock is loaded and HttpFake.configuration.auto_intercept is true,
|
|
7
|
+
# requests to the declared base_url are intercepted transparently.
|
|
8
|
+
#
|
|
9
|
+
# Teardown removes only the stub httpfake created — never calls WebMock.reset!.
|
|
10
|
+
# If WebMock/RSpec has already cleared the registry (its own after(:each) hook),
|
|
11
|
+
# we rescue silently rather than crashing.
|
|
12
|
+
module WebMockIntegration
|
|
13
|
+
class << self
|
|
14
|
+
def available?
|
|
15
|
+
defined?(WebMock) && WebMock.respond_to?(:stub_request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Install an interception stub for the given server.
|
|
19
|
+
# Returns the stub object so it can be removed precisely during teardown.
|
|
20
|
+
def setup(server)
|
|
21
|
+
return nil unless available? && HttpFake.configuration.auto_intercept
|
|
22
|
+
return nil unless server.route_map.declared_base_url
|
|
23
|
+
|
|
24
|
+
# Match the full base URL (scheme + host) so the regex anchors correctly.
|
|
25
|
+
# e.g. "https://api.stripe.com" → /\Ahttps:\/\/api\.stripe\.com/
|
|
26
|
+
base = server.route_map.declared_base_url.chomp("/")
|
|
27
|
+
pattern = /\A#{Regexp.escape(base)}/
|
|
28
|
+
|
|
29
|
+
WebMock.stub_request(:any, pattern).to_rack(server.rack_app)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Remove only the stub we created.
|
|
33
|
+
# Rescues silently if webmock/rspec already cleared the registry between examples.
|
|
34
|
+
def teardown(stub)
|
|
35
|
+
return unless stub && available?
|
|
36
|
+
|
|
37
|
+
WebMock::StubRegistry.instance.remove_request_stub(stub)
|
|
38
|
+
rescue RuntimeError
|
|
39
|
+
# Already removed by WebMock.reset! (e.g. webmock/rspec after(:each) hook). Fine.
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/httpfake.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "httpfake/version"
|
|
4
|
+
require_relative "httpfake/configuration"
|
|
5
|
+
require_relative "httpfake/route"
|
|
6
|
+
require_relative "httpfake/route_map"
|
|
7
|
+
require_relative "httpfake/router"
|
|
8
|
+
require_relative "httpfake/request_log"
|
|
9
|
+
require_relative "httpfake/handler_context"
|
|
10
|
+
require_relative "httpfake/server"
|
|
11
|
+
require_relative "httpfake/webmock_integration"
|
|
12
|
+
|
|
13
|
+
module HttpFake
|
|
14
|
+
class << self
|
|
15
|
+
# Global configuration.
|
|
16
|
+
#
|
|
17
|
+
# HttpFake.configure do |c|
|
|
18
|
+
# c.auto_intercept = false # opt out of WebMock auto-interception
|
|
19
|
+
# end
|
|
20
|
+
def configure
|
|
21
|
+
yield configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Define a named fake service.
|
|
29
|
+
#
|
|
30
|
+
# FakeStripe = HttpFake.define(:stripe) do
|
|
31
|
+
# base_url "https://api.stripe.com"
|
|
32
|
+
#
|
|
33
|
+
# post "/v1/charges" do
|
|
34
|
+
# requires_body :amount, :currency, :payment_method
|
|
35
|
+
# respond 200, json: { id: -> { "ch_#{SecureRandom.hex(8)}" } }
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# RSpec.configure { |c| c.include FakeStripe.rspec_helpers }
|
|
40
|
+
#
|
|
41
|
+
def define(name = :default, &)
|
|
42
|
+
require_relative "httpfake/rspec"
|
|
43
|
+
route_map = RouteMap.new
|
|
44
|
+
route_map.instance_eval(&)
|
|
45
|
+
Definition.new(name, route_map)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/sig/httpfake.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: http_decoy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jibran Usman
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rack
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webrick
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.8'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.8'
|
|
41
|
+
description: httpfake spins up a real Rack server inside your tests with a clean DSL.
|
|
42
|
+
Define routes, validate request contracts, return dynamic fixtures, and tear down
|
|
43
|
+
automatically. No VCR cassettes. No scattered WebMock stubs.
|
|
44
|
+
email:
|
|
45
|
+
- jibran.usman@hotmail.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- ".rubocop.yml"
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- CONTRIBUTING.md
|
|
53
|
+
- LICENSE
|
|
54
|
+
- README.md
|
|
55
|
+
- Rakefile
|
|
56
|
+
- lib/httpfake.rb
|
|
57
|
+
- lib/httpfake/configuration.rb
|
|
58
|
+
- lib/httpfake/handler_context.rb
|
|
59
|
+
- lib/httpfake/request_log.rb
|
|
60
|
+
- lib/httpfake/route.rb
|
|
61
|
+
- lib/httpfake/route_map.rb
|
|
62
|
+
- lib/httpfake/router.rb
|
|
63
|
+
- lib/httpfake/rspec.rb
|
|
64
|
+
- lib/httpfake/server.rb
|
|
65
|
+
- lib/httpfake/version.rb
|
|
66
|
+
- lib/httpfake/webmock_integration.rb
|
|
67
|
+
- sig/httpfake.rbs
|
|
68
|
+
homepage: https://github.com/jibranusman95/httpfake
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata:
|
|
72
|
+
homepage_uri: https://github.com/jibranusman95/httpfake
|
|
73
|
+
source_code_uri: https://github.com/jibranusman95/httpfake
|
|
74
|
+
changelog_uri: https://github.com/jibranusman95/httpfake/blob/main/CHANGELOG.md
|
|
75
|
+
rubygems_mfa_required: 'true'
|
|
76
|
+
post_install_message:
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 3.1.0
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 3.5.22
|
|
92
|
+
signing_key:
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Declarative fake HTTP servers for RSpec. Real server. Real requests. Zero
|
|
95
|
+
cassettes.
|
|
96
|
+
test_files: []
|