bugsnag-maze-runner 7.22.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/bugsnag-print-load-paths +6 -0
- data/bin/download-logs +74 -0
- data/bin/maze-runner +174 -0
- data/bin/upload-app +56 -0
- data/lib/features/scripts/await-android-emulator.sh +11 -0
- data/lib/features/scripts/clear-android-app-data.sh +8 -0
- data/lib/features/scripts/force-stop-android-app.sh +8 -0
- data/lib/features/scripts/install-android-app.sh +15 -0
- data/lib/features/scripts/launch-android-app.sh +38 -0
- data/lib/features/scripts/launch-android-emulator.sh +15 -0
- data/lib/features/steps/android_steps.rb +51 -0
- data/lib/features/steps/app_automator_steps.rb +228 -0
- data/lib/features/steps/aws_sam_steps.rb +212 -0
- data/lib/features/steps/breadcrumb_steps.rb +80 -0
- data/lib/features/steps/browser_steps.rb +93 -0
- data/lib/features/steps/build_api_steps.rb +25 -0
- data/lib/features/steps/document_server_steps.rb +7 -0
- data/lib/features/steps/error_reporting_steps.rb +358 -0
- data/lib/features/steps/feature_flag_steps.rb +190 -0
- data/lib/features/steps/header_steps.rb +72 -0
- data/lib/features/steps/log_steps.rb +29 -0
- data/lib/features/steps/multipart_request_steps.rb +142 -0
- data/lib/features/steps/network_steps.rb +135 -0
- data/lib/features/steps/payload_steps.rb +257 -0
- data/lib/features/steps/proxy_steps.rb +34 -0
- data/lib/features/steps/query_parameter_steps.rb +31 -0
- data/lib/features/steps/request_assertion_steps.rb +186 -0
- data/lib/features/steps/runner_steps.rb +428 -0
- data/lib/features/steps/session_tracking_steps.rb +116 -0
- data/lib/features/steps/trace_steps.rb +206 -0
- data/lib/features/steps/value_steps.rb +119 -0
- data/lib/features/support/env.rb +7 -0
- data/lib/features/support/internal_hooks.rb +207 -0
- data/lib/maze/api/appium/file_manager.rb +29 -0
- data/lib/maze/appium_server.rb +112 -0
- data/lib/maze/assertions/request_set_assertions.rb +97 -0
- data/lib/maze/aws/sam.rb +112 -0
- data/lib/maze/aws_public_ip.rb +53 -0
- data/lib/maze/bugsnag_config.rb +42 -0
- data/lib/maze/checks/assert_check.rb +69 -0
- data/lib/maze/checks/noop_check.rb +34 -0
- data/lib/maze/client/appium/base_client.rb +131 -0
- data/lib/maze/client/appium/bb_client.rb +102 -0
- data/lib/maze/client/appium/bb_devices.rb +127 -0
- data/lib/maze/client/appium/bs_client.rb +91 -0
- data/lib/maze/client/appium/bs_devices.rb +141 -0
- data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
- data/lib/maze/client/appium/local_client.rb +67 -0
- data/lib/maze/client/appium.rb +23 -0
- data/lib/maze/client/bb_api_client.rb +102 -0
- data/lib/maze/client/bb_client_utils.rb +181 -0
- data/lib/maze/client/bs_client_utils.rb +168 -0
- data/lib/maze/client/selenium/base_client.rb +15 -0
- data/lib/maze/client/selenium/bb_browsers.yml +188 -0
- data/lib/maze/client/selenium/bb_client.rb +38 -0
- data/lib/maze/client/selenium/bs_browsers.yml +257 -0
- data/lib/maze/client/selenium/bs_client.rb +89 -0
- data/lib/maze/client/selenium/local_client.rb +16 -0
- data/lib/maze/client/selenium.rb +16 -0
- data/lib/maze/compare.rb +161 -0
- data/lib/maze/configuration.rb +182 -0
- data/lib/maze/docker.rb +147 -0
- data/lib/maze/document_server.rb +46 -0
- data/lib/maze/driver/appium.rb +198 -0
- data/lib/maze/driver/browser.rb +124 -0
- data/lib/maze/errors.rb +52 -0
- data/lib/maze/generator.rb +55 -0
- data/lib/maze/helper.rb +122 -0
- data/lib/maze/hooks/appium_hooks.rb +55 -0
- data/lib/maze/hooks/browser_hooks.rb +15 -0
- data/lib/maze/hooks/command_hooks.rb +9 -0
- data/lib/maze/hooks/error_code_hook.rb +49 -0
- data/lib/maze/hooks/hooks.rb +61 -0
- data/lib/maze/http_request.rb +21 -0
- data/lib/maze/interactive_cli.rb +173 -0
- data/lib/maze/logger.rb +86 -0
- data/lib/maze/macos_utils.rb +14 -0
- data/lib/maze/maze_output.rb +88 -0
- data/lib/maze/network.rb +49 -0
- data/lib/maze/option/parser.rb +240 -0
- data/lib/maze/option/processor.rb +130 -0
- data/lib/maze/option/validator.rb +155 -0
- data/lib/maze/option.rb +62 -0
- data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
- data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
- data/lib/maze/plugins/error_code_plugin.rb +21 -0
- data/lib/maze/plugins/global_retry_plugin.rb +38 -0
- data/lib/maze/proxy.rb +114 -0
- data/lib/maze/request_list.rb +87 -0
- data/lib/maze/request_repeater.rb +49 -0
- data/lib/maze/retry_handler.rb +67 -0
- data/lib/maze/runner.rb +149 -0
- data/lib/maze/schemas/OtelTraceSchema.json +390 -0
- data/lib/maze/schemas/trace_schema.rb +7 -0
- data/lib/maze/schemas/trace_validator.rb +98 -0
- data/lib/maze/server.rb +251 -0
- data/lib/maze/servlets/base_servlet.rb +27 -0
- data/lib/maze/servlets/command_servlet.rb +47 -0
- data/lib/maze/servlets/log_servlet.rb +64 -0
- data/lib/maze/servlets/reflective_servlet.rb +70 -0
- data/lib/maze/servlets/servlet.rb +199 -0
- data/lib/maze/servlets/temp.rb +0 -0
- data/lib/maze/servlets/trace_servlet.rb +13 -0
- data/lib/maze/store.rb +15 -0
- data/lib/maze/terminating_server.rb +129 -0
- data/lib/maze/timers.rb +51 -0
- data/lib/maze/wait.rb +35 -0
- data/lib/maze.rb +27 -0
- data/lib/utils/deep_merge.rb +17 -0
- data/lib/utils/selenium_money_patch.rb +17 -0
- metadata +451 -0
data/lib/maze/server.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'webrick'
|
6
|
+
require_relative './logger'
|
7
|
+
require_relative './request_list'
|
8
|
+
|
9
|
+
module Maze
|
10
|
+
# Receives and stores requests through a WEBrick HTTPServer
|
11
|
+
class Server
|
12
|
+
ALLOWED_HTTP_VERBS = %w[OPTIONS GET POST PUT DELETE HEAD TRACE PATCH CONNECT]
|
13
|
+
DEFAULT_RESPONSE_DELAY = 0
|
14
|
+
DEFAULT_SAMPLING_PROBABILITY = 1
|
15
|
+
DEFAULT_STATUS_CODE = 200
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Sets the response delay generator.
|
19
|
+
#
|
20
|
+
# @param generator [Maze::Generator] The new generator
|
21
|
+
def set_response_delay_generator(generator)
|
22
|
+
@response_delay_generator&.close
|
23
|
+
@response_delay_generator = generator
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets the sampling probability generator.
|
27
|
+
#
|
28
|
+
# @param generator [Maze::Generator] The new generator
|
29
|
+
def set_sampling_probability_generator(generator)
|
30
|
+
@sampling_probability_generator&.close
|
31
|
+
@sampling_probability_generator = generator
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets the status code generator for the HTTP verb given. If no verb is given then the
|
35
|
+
# generator will be shared across all allowable HTTP verbs.
|
36
|
+
#
|
37
|
+
# @param generator [Maze::Generator] The new generator
|
38
|
+
# @param verb [String] HTTP verb
|
39
|
+
def set_status_code_generator(generator, verb = nil)
|
40
|
+
@status_code_generators ||= {}
|
41
|
+
Array(verb || ALLOWED_HTTP_VERBS).each do |verb|
|
42
|
+
old = @status_code_generators[verb]
|
43
|
+
@status_code_generators[verb] = generator
|
44
|
+
|
45
|
+
# Close the old generator unless it's still being used by another verb
|
46
|
+
old&.close unless @status_code_generators.value?(old)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# The intended HTTP status code on a successful request
|
51
|
+
#
|
52
|
+
# @param verb [String] HTTP verb for which the status code is wanted
|
53
|
+
#
|
54
|
+
# @return [Integer] The HTTP status code for the verb given
|
55
|
+
def status_code(verb)
|
56
|
+
if @status_code_generators[verb].nil? || @status_code_generators[verb].closed?
|
57
|
+
DEFAULT_STATUS_CODE
|
58
|
+
else
|
59
|
+
@status_code_generators[verb].next
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def sampling_probability
|
64
|
+
@sampling_probability_generator.next
|
65
|
+
end
|
66
|
+
|
67
|
+
def response_delay_ms
|
68
|
+
@response_delay_generator.next
|
69
|
+
end
|
70
|
+
|
71
|
+
# Provides dynamic access to request lists by name
|
72
|
+
#
|
73
|
+
# @param type [String, Symbol] Request type
|
74
|
+
# @return Request list for the type given
|
75
|
+
def list_for(type)
|
76
|
+
type = type.to_s
|
77
|
+
case type
|
78
|
+
when 'error', 'errors'
|
79
|
+
errors
|
80
|
+
when 'session', 'sessions'
|
81
|
+
sessions
|
82
|
+
when 'build', 'builds'
|
83
|
+
builds
|
84
|
+
when 'log', 'logs'
|
85
|
+
logs
|
86
|
+
when 'trace', 'traces'
|
87
|
+
traces
|
88
|
+
when 'upload', 'uploads'
|
89
|
+
uploads
|
90
|
+
when 'sourcemap', 'sourcemaps'
|
91
|
+
sourcemaps
|
92
|
+
when 'invalid', 'invalid requests'
|
93
|
+
invalid_requests
|
94
|
+
else
|
95
|
+
raise "Invalid request type '#{type}'"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# A list of error requests received
|
100
|
+
#
|
101
|
+
# @return [RequestList] Received error requests
|
102
|
+
def errors
|
103
|
+
@errors ||= RequestList.new
|
104
|
+
end
|
105
|
+
|
106
|
+
# A list of session requests received
|
107
|
+
#
|
108
|
+
# @return [RequestList] Received error requests
|
109
|
+
def sessions
|
110
|
+
@sessions ||= RequestList.new
|
111
|
+
end
|
112
|
+
|
113
|
+
# A list of trace requests received
|
114
|
+
#
|
115
|
+
# @return [RequestList] Received error requests
|
116
|
+
def traces
|
117
|
+
@traces ||= RequestList.new
|
118
|
+
end
|
119
|
+
|
120
|
+
# A list of build requests received
|
121
|
+
#
|
122
|
+
# @return [RequestList] Received build requests
|
123
|
+
def builds
|
124
|
+
@builds ||= RequestList.new
|
125
|
+
end
|
126
|
+
|
127
|
+
# A list of upload requests received
|
128
|
+
#
|
129
|
+
# @return [RequestList] Received upload requests
|
130
|
+
def uploads
|
131
|
+
@uploads ||= RequestList.new
|
132
|
+
end
|
133
|
+
|
134
|
+
# A list of sourcemap requests received
|
135
|
+
#
|
136
|
+
# @return [RequestList] Received sourcemap requests
|
137
|
+
def sourcemaps
|
138
|
+
@sourcemaps ||= RequestList.new
|
139
|
+
end
|
140
|
+
|
141
|
+
# A list of log requests received
|
142
|
+
#
|
143
|
+
# @return [RequestList] Received log requests
|
144
|
+
def logs
|
145
|
+
@logs ||= RequestList.new
|
146
|
+
end
|
147
|
+
|
148
|
+
# A list of commands for a test fixture to perform. Strictly speaking these are responses to HTTP
|
149
|
+
# requests, but the list behavior is all we need.
|
150
|
+
#
|
151
|
+
# @return [RequestList] Commands to be performed
|
152
|
+
def commands
|
153
|
+
@commands ||= RequestList.new
|
154
|
+
end
|
155
|
+
|
156
|
+
# Whether the server thread is running
|
157
|
+
# An array of any invalid requests received.
|
158
|
+
# Each request is hash consisting of:
|
159
|
+
# request: The original HTTPRequest object
|
160
|
+
# reason: Reason for being considered invalid. Examples include invalid JSON and missing/invalid digest.
|
161
|
+
# @return [RequestList] An array of received requests
|
162
|
+
def invalid_requests
|
163
|
+
@invalid_requests ||= RequestList.new
|
164
|
+
end
|
165
|
+
|
166
|
+
# Whether the server thread is running
|
167
|
+
#
|
168
|
+
# @return [Boolean] If the server is running
|
169
|
+
def running?
|
170
|
+
@thread&.alive?
|
171
|
+
end
|
172
|
+
|
173
|
+
# Starts the WEBrick server in a separate thread
|
174
|
+
def start
|
175
|
+
attempts = 0
|
176
|
+
loop do
|
177
|
+
|
178
|
+
@thread = Thread.new do
|
179
|
+
options = {
|
180
|
+
Port: Maze.config.port,
|
181
|
+
Logger: $logger,
|
182
|
+
AccessLog: []
|
183
|
+
}
|
184
|
+
options[:BindAddress] = Maze.config.bind_address unless Maze.config.bind_address.nil?
|
185
|
+
server = WEBrick::HTTPServer.new(options)
|
186
|
+
|
187
|
+
# Mount a block to respond to all requests with status:200
|
188
|
+
server.mount_proc '/' do |_request, response|
|
189
|
+
$logger.debug 'Received request on server root, responding with 200'
|
190
|
+
response.header['Access-Control-Allow-Origin'] = '*'
|
191
|
+
response.body = 'Maze runner received request'
|
192
|
+
response.status = 200
|
193
|
+
end
|
194
|
+
|
195
|
+
# When adding more endpoints, be sure to update the 'I should receive no requests' step
|
196
|
+
server.mount '/notify', Servlets::Servlet, :errors
|
197
|
+
server.mount '/sessions', Servlets::Servlet, :sessions
|
198
|
+
server.mount '/builds', Servlets::Servlet, :builds
|
199
|
+
server.mount '/uploads', Servlets::Servlet, :uploads
|
200
|
+
server.mount '/sourcemap', Servlets::Servlet, :sourcemaps
|
201
|
+
server.mount '/traces', Servlets::TraceServlet, :traces, Maze::Schemas::TRACE_SCHEMA
|
202
|
+
server.mount '/react-native-source-map', Servlets::Servlet, :sourcemaps
|
203
|
+
server.mount '/command', Servlets::CommandServlet
|
204
|
+
server.mount '/logs', Servlets::LogServlet
|
205
|
+
server.mount '/reflect', Servlets::ReflectiveServlet
|
206
|
+
server.start
|
207
|
+
rescue StandardError => e
|
208
|
+
$logger.warn "Failed to start mock server: #{e.message}"
|
209
|
+
ensure
|
210
|
+
server&.shutdown
|
211
|
+
end
|
212
|
+
|
213
|
+
# Need a short sleep here as a dying thread is still alive momentarily
|
214
|
+
sleep 1
|
215
|
+
break if running?
|
216
|
+
|
217
|
+
# Bail out after 3 attempts
|
218
|
+
attempts += 1
|
219
|
+
raise 'Too many failed attempts to start mock server' if attempts == 3
|
220
|
+
|
221
|
+
# Failed to start - sleep before retrying
|
222
|
+
$logger.info 'Retrying in 5 seconds'
|
223
|
+
sleep 5
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Stops the WEBrick server thread if it's running
|
228
|
+
def stop
|
229
|
+
@thread&.kill if @thread&.alive?
|
230
|
+
@thread = nil
|
231
|
+
end
|
232
|
+
|
233
|
+
def reset!
|
234
|
+
# Reset generators
|
235
|
+
set_response_delay_generator(Maze::Generator.new [DEFAULT_RESPONSE_DELAY].cycle)
|
236
|
+
set_status_code_generator(Maze::Generator.new [DEFAULT_STATUS_CODE].cycle)
|
237
|
+
set_sampling_probability_generator(Maze::Generator.new [DEFAULT_SAMPLING_PROBABILITY].cycle)
|
238
|
+
|
239
|
+
# Clear request lists
|
240
|
+
errors.clear
|
241
|
+
sessions.clear
|
242
|
+
builds.clear
|
243
|
+
uploads.clear
|
244
|
+
sourcemaps.clear
|
245
|
+
traces.clear
|
246
|
+
logs.clear
|
247
|
+
invalid_requests.clear
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Servlets
|
5
|
+
|
6
|
+
# Base servlet to avoid duplication of common code
|
7
|
+
class BaseServlet < WEBrick::HTTPServlet::AbstractServlet
|
8
|
+
# Logs and returns a set of valid headers for this servlet.
|
9
|
+
#
|
10
|
+
# @param request [HTTPRequest] The incoming GET request
|
11
|
+
# @param response [HTTPResponse] The response to return
|
12
|
+
def do_OPTIONS(request, response)
|
13
|
+
response.header['Access-Control-Allow-Origin'] = '*'
|
14
|
+
response.header['Access-Control-Allow-Headers'] = %w[
|
15
|
+
Accept
|
16
|
+
Bugsnag-Api-Key
|
17
|
+
Bugsnag-Integrity
|
18
|
+
Bugsnag-Payload-Version
|
19
|
+
Bugsnag-Sent-At
|
20
|
+
Bugsnag-Span-Sampling
|
21
|
+
Content-Type
|
22
|
+
Origin
|
23
|
+
].join(',')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Maze
|
6
|
+
module Servlets
|
7
|
+
|
8
|
+
# Allows clients to queue up "commands", in the form of Ruby hashes, using Maze::Server.commands.add. GET
|
9
|
+
# requests made to the /command endpoint will then respond with each queued command in turn.
|
10
|
+
class CommandServlet < BaseServlet
|
11
|
+
# Serves the next command, if these is one.
|
12
|
+
#
|
13
|
+
# @param _request [HTTPRequest] The incoming GET request
|
14
|
+
# @param response [HTTPResponse] The response to return
|
15
|
+
def do_GET(_request, response)
|
16
|
+
response.header['Access-Control-Allow-Origin'] = '*'
|
17
|
+
|
18
|
+
commands = Maze::Server.commands
|
19
|
+
|
20
|
+
if commands.size_remaining == 0
|
21
|
+
response.body = '{"action": "noop", "message": "No commands queued"}'
|
22
|
+
response.status = 200
|
23
|
+
else
|
24
|
+
command = commands.current
|
25
|
+
command_json = JSON.pretty_generate(command)
|
26
|
+
command[:uuid] = Maze.run_uuid
|
27
|
+
response.body = command_json
|
28
|
+
response.status = 200
|
29
|
+
commands.next
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Logs and returns a set of valid headers for this servlet.
|
34
|
+
#
|
35
|
+
# @param request [HTTPRequest] The incoming GET request
|
36
|
+
# @param response [HTTPResponse] The response to return
|
37
|
+
def do_OPTIONS(request, response)
|
38
|
+
super
|
39
|
+
|
40
|
+
response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
|
41
|
+
response.status = Server.status_code('OPTIONS')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Servlets
|
5
|
+
|
6
|
+
# Receives log requests sent from the test fixture
|
7
|
+
class LogServlet < BaseServlet
|
8
|
+
# Constructor
|
9
|
+
#
|
10
|
+
# @param server [HTTPServer] WEBrick HTTPServer
|
11
|
+
def initialize(server)
|
12
|
+
super server
|
13
|
+
@requests = Server.logs
|
14
|
+
end
|
15
|
+
|
16
|
+
# Logs and parses an incoming POST request.
|
17
|
+
# Parses `multipart/form-data` and `application/json` content-types.
|
18
|
+
# Parsed requests are added to the requests list.
|
19
|
+
#
|
20
|
+
# @param request [HTTPRequest] The incoming GET request
|
21
|
+
# @param response [HTTPResponse] The response to return
|
22
|
+
def do_POST(request, response)
|
23
|
+
hash = {
|
24
|
+
body: JSON.parse(request.body),
|
25
|
+
request: request
|
26
|
+
}
|
27
|
+
@requests.add(hash)
|
28
|
+
|
29
|
+
response.header['Access-Control-Allow-Origin'] = '*'
|
30
|
+
response.status = Server.status_code('POST')
|
31
|
+
rescue JSON::ParserError => e
|
32
|
+
msg = "Unable to parse request as JSON: #{e.message}"
|
33
|
+
$logger.error msg
|
34
|
+
Server.invalid_requests.add({
|
35
|
+
reason: msg,
|
36
|
+
request: request,
|
37
|
+
body: request.body
|
38
|
+
})
|
39
|
+
rescue StandardError => e
|
40
|
+
$logger.error "Invalid request: #{e.message}"
|
41
|
+
Server.invalid_requests.add({
|
42
|
+
invalid: true,
|
43
|
+
reason: e.message,
|
44
|
+
request: {
|
45
|
+
request_uri: request.request_uri,
|
46
|
+
header: request.header.to_h,
|
47
|
+
body: request.inspect
|
48
|
+
}
|
49
|
+
})
|
50
|
+
end
|
51
|
+
|
52
|
+
# Logs and returns a set of valid headers for this servlet.
|
53
|
+
#
|
54
|
+
# @param request [HTTPRequest] The incoming GET request
|
55
|
+
# @param response [HTTPResponse] The response to return
|
56
|
+
def do_OPTIONS(request, response)
|
57
|
+
super
|
58
|
+
|
59
|
+
response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
|
60
|
+
response.status = Server.status_code('OPTIONS')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Maze
|
5
|
+
module Servlets
|
6
|
+
# Receives HTTP requests and responds according to the parameters given, which are:
|
7
|
+
# - delay_ms - milliseconds to wait before responding
|
8
|
+
# - status - HTTP response code
|
9
|
+
# For GET requests these are expected to passed as GET parameters,
|
10
|
+
# for POST requests they are expected to be given as JSON fields.
|
11
|
+
class ReflectiveServlet < BaseServlet
|
12
|
+
|
13
|
+
# Accepts a GET request to provide a reflective response to.
|
14
|
+
#
|
15
|
+
# @param request [HTTPRequest] The incoming GET request
|
16
|
+
# @param response [HTTPResponse] The response to return
|
17
|
+
def do_GET(request, response)
|
18
|
+
delay_ms = request.query['delay_ms']
|
19
|
+
status = request.query['status']
|
20
|
+
reflect response, delay_ms, status
|
21
|
+
end
|
22
|
+
|
23
|
+
# Accepts a POST request to provide a reflective response to.
|
24
|
+
#
|
25
|
+
# @param request [HTTPRequest] The incoming GET request
|
26
|
+
# @param response [HTTPResponse] The response to return
|
27
|
+
def do_POST(request, response)
|
28
|
+
|
29
|
+
content_type = request['Content-Type']
|
30
|
+
|
31
|
+
# For JSON, pull the instructions from the body. Otherwise, take them from the query string.
|
32
|
+
if content_type == 'application/json'
|
33
|
+
body = JSON.parse(request.body)
|
34
|
+
delay_ms = body['delay_ms']
|
35
|
+
status = body['status']
|
36
|
+
else
|
37
|
+
query = Rack::Utils.parse_nested_query(request.query_string)
|
38
|
+
delay_ms = query['delay_ms']
|
39
|
+
status = query['status']
|
40
|
+
end
|
41
|
+
|
42
|
+
reflect response, delay_ms, status
|
43
|
+
rescue JSON::ParserError => e
|
44
|
+
msg = "Unable to parse request as JSON: #{e.message}"
|
45
|
+
$logger.error msg
|
46
|
+
response.status = 418
|
47
|
+
rescue StandardError => e
|
48
|
+
$logger.error "Invalid request: #{e.message}"
|
49
|
+
response.status = 500
|
50
|
+
end
|
51
|
+
|
52
|
+
def reflect(response, delay_ms, status)
|
53
|
+
sleep delay_ms.to_i / 1000 unless delay_ms.nil?
|
54
|
+
response.status = status || 200
|
55
|
+
response.header['Access-Control-Allow-Origin'] = '*'
|
56
|
+
response.body = "Returned status #{status} after waiting #{delay_ms} ms"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Logs and returns a set of valid headers for this servlet.
|
60
|
+
#
|
61
|
+
# @param request [HTTPRequest] The incoming GET request
|
62
|
+
# @param response [HTTPResponse] The response to return
|
63
|
+
def do_OPTIONS(request, response)
|
64
|
+
super
|
65
|
+
|
66
|
+
response.header['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zlib'
|
4
|
+
require 'stringio'
|
5
|
+
require 'json_schemer'
|
6
|
+
require 'delegate'
|
7
|
+
|
8
|
+
module Maze
|
9
|
+
class HttpRequest < SimpleDelegator
|
10
|
+
def body
|
11
|
+
@body ||= decode_body
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def decode_body
|
17
|
+
delegate = __getobj__
|
18
|
+
if %r{^gzip$}.match(delegate['Content-Encoding'])
|
19
|
+
gz_element = Zlib::GzipReader.new(StringIO.new(delegate.body))
|
20
|
+
gz_element.read
|
21
|
+
else
|
22
|
+
delegate.body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Servlets
|
28
|
+
|
29
|
+
# Receives and parses the requests and payloads sent from the test fixture
|
30
|
+
class Servlet < BaseServlet
|
31
|
+
prepend RequestRepeater
|
32
|
+
|
33
|
+
# Constructor
|
34
|
+
#
|
35
|
+
# @param server [HTTPServer] WEBrick HTTPServer
|
36
|
+
# @param request_type [Symbol] Request type that the servlet will receive
|
37
|
+
# @param schema [Dictionary] A `json-schema` describing the payload for POST requests
|
38
|
+
def initialize(server, request_type, schema=nil)
|
39
|
+
super server
|
40
|
+
@request_type = request_type
|
41
|
+
@requests = Server.list_for request_type
|
42
|
+
@schema = JSONSchemer.schema(schema) unless schema.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Logs an incoming GET WEBrick request.
|
46
|
+
#
|
47
|
+
# @param request [HTTPRequest] The incoming GET request
|
48
|
+
# @param _response [HTTPResponse] The response to return
|
49
|
+
def do_GET(request, _response)
|
50
|
+
log_request(request)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Logs and parses an incoming POST request.
|
54
|
+
# Parses `multipart/form-data` and `application/json` content-types.
|
55
|
+
# Parsed requests are added to the requests list.
|
56
|
+
#
|
57
|
+
# @param request [HTTPRequest] The incoming GET request
|
58
|
+
# @param response [HTTPResponse] The response to return
|
59
|
+
def do_POST(request, response)
|
60
|
+
# Turn the WEBrick HttpRequest into our internal HttpRequest delegate
|
61
|
+
request = HttpRequest.new(request)
|
62
|
+
|
63
|
+
content_type = request['Content-Type']
|
64
|
+
if %r{^multipart/form-data; boundary=([^;]+)}.match(content_type)
|
65
|
+
boundary = WEBrick::HTTPUtils::dequote($1)
|
66
|
+
body = WEBrick::HTTPUtils.parse_form_data(request.body, boundary)
|
67
|
+
hash = {
|
68
|
+
body: body,
|
69
|
+
request: request
|
70
|
+
}
|
71
|
+
else
|
72
|
+
# "content-type" is assumed to be JSON (which mimics the behaviour of
|
73
|
+
# the actual API). This supports browsers that can't set this header for
|
74
|
+
# cross-domain requests (IE8/9)
|
75
|
+
digests = check_digest request
|
76
|
+
hash = {
|
77
|
+
body: JSON.parse(request.body),
|
78
|
+
request: request,
|
79
|
+
digests: digests
|
80
|
+
}
|
81
|
+
end
|
82
|
+
if @schema
|
83
|
+
schema_errors = @schema.validate(hash[:body])
|
84
|
+
hash[:schema_errors] = schema_errors.to_a
|
85
|
+
end
|
86
|
+
@requests.add(hash)
|
87
|
+
|
88
|
+
# For the response, delaying if configured to do so
|
89
|
+
response_delay_ms = Server.response_delay_ms
|
90
|
+
if response_delay_ms.positive?
|
91
|
+
$logger.info "Waiting #{response_delay_ms} milliseconds before responding"
|
92
|
+
sleep response_delay_ms / 1000.0
|
93
|
+
end
|
94
|
+
set_response_header response.header
|
95
|
+
response.status = Server.status_code('POST')
|
96
|
+
rescue JSON::ParserError => e
|
97
|
+
msg = "Unable to parse request as JSON: #{e.message}"
|
98
|
+
if Maze.config.captured_invalid_requests.include? @request_type
|
99
|
+
$logger.error msg
|
100
|
+
Server.invalid_requests.add({
|
101
|
+
reason: msg,
|
102
|
+
request: request,
|
103
|
+
body: request.body
|
104
|
+
})
|
105
|
+
else
|
106
|
+
$logger.warn msg
|
107
|
+
end
|
108
|
+
rescue StandardError => e
|
109
|
+
if Maze.config.captured_invalid_requests.include? @request_type
|
110
|
+
$logger.error "Invalid request: #{e.message}"
|
111
|
+
Server.invalid_requests.add({
|
112
|
+
invalid: true,
|
113
|
+
reason: e.message,
|
114
|
+
request: {
|
115
|
+
request_uri: request.request_uri,
|
116
|
+
header: request.header,
|
117
|
+
body: request.inspect
|
118
|
+
}
|
119
|
+
})
|
120
|
+
else
|
121
|
+
$logger.warn "Invalid request: #{e.message}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_response_header(header)
|
126
|
+
header['Access-Control-Allow-Origin'] = '*'
|
127
|
+
end
|
128
|
+
|
129
|
+
# Logs and returns a set of valid headers for this servlet.
|
130
|
+
#
|
131
|
+
# @param request [HTTPRequest] The incoming GET request
|
132
|
+
# @param response [HTTPResponse] The response to return
|
133
|
+
def do_OPTIONS(request, response)
|
134
|
+
super
|
135
|
+
|
136
|
+
response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
|
137
|
+
response.status = Server.status_code('OPTIONS')
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def log_request(request)
|
143
|
+
$logger.debug "#{request.request_method} request received"
|
144
|
+
$logger.debug "URI: #{request.unparsed_uri}"
|
145
|
+
$logger.debug "HEADERS: #{request.raw_header}"
|
146
|
+
return if request.body.nil?
|
147
|
+
|
148
|
+
case request['Content-Type']
|
149
|
+
when nil
|
150
|
+
nil
|
151
|
+
when %r{^multipart/form-data; boundary=([^;]+)}
|
152
|
+
boundary = WEBrick::HTTPUtils.dequote(Regexp.last_match(1))
|
153
|
+
body = WEBrick::HTTPUtils.parse_form_data(request.body, boundary)
|
154
|
+
$logger.debug 'BODY:'
|
155
|
+
LogUtil.log_hash(Logger::Severity::DEBUG, body)
|
156
|
+
when %r{^application/json$}
|
157
|
+
$logger.debug "BODY: #{JSON.pretty_generate(JSON.parse(request.body))}"
|
158
|
+
else
|
159
|
+
$logger.debug "BODY: #{request.body}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Checks the Bugsnag-Integrity header, if present, against the request and based on configuration.
|
164
|
+
# If the header is present, if the digest must be correct. However, the header need only be present
|
165
|
+
# if configuration says so.
|
166
|
+
def check_digest(request)
|
167
|
+
header = request['Bugsnag-Integrity']
|
168
|
+
if header.nil? && Maze.config.enforce_bugsnag_integrity
|
169
|
+
raise 'Bugsnag-Integrity header must be present according to Maze.config.enforce_bugsnag_integrity'
|
170
|
+
end
|
171
|
+
return if header.nil?
|
172
|
+
|
173
|
+
# Header must have type and digest
|
174
|
+
parts = header.split ' '
|
175
|
+
raise "Invalid Bugsnag-Integrity header: #{header}" unless parts.size == 2
|
176
|
+
|
177
|
+
# Both digest types are stored whatever
|
178
|
+
sha1 = Digest::SHA1.hexdigest(request.body)
|
179
|
+
simple = request.body.bytesize
|
180
|
+
$logger.debug "DIGESTS computed: sha1=#{sha1} simple=#{simple}"
|
181
|
+
|
182
|
+
# Check digests match
|
183
|
+
case parts[0]
|
184
|
+
when 'sha1'
|
185
|
+
raise "Given sha1 #{parts[1]} does not match the computed #{sha1}" unless parts[1] == sha1
|
186
|
+
when 'simple'
|
187
|
+
raise "Given simple digest #{parts[1].inspect} does not match the computed #{simple.inspect}" unless parts[1].to_i == simple
|
188
|
+
else
|
189
|
+
raise "Invalid Bugsnag-Integrity digest type: #{parts[0]}"
|
190
|
+
end
|
191
|
+
|
192
|
+
{
|
193
|
+
sha1: sha1,
|
194
|
+
simple: simple
|
195
|
+
}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Servlets
|
5
|
+
class TraceServlet < Servlet
|
6
|
+
def set_response_header(header)
|
7
|
+
super
|
8
|
+
value = Maze::Server.sampling_probability
|
9
|
+
header['Bugsnag-Sampling-Probability'] = value unless value == 'null'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/maze/store.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
# Provides a hash to store values for cross-request comparisons
|
5
|
+
class Store
|
6
|
+
class << self
|
7
|
+
# Returns the hash of stored values for the current scenario
|
8
|
+
#
|
9
|
+
# @return [Hash] The stored value hash
|
10
|
+
def values
|
11
|
+
@values ||= {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|