bugsnag-maze-runner 6.27.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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +76 -0
  4. data/bin/maze-runner +136 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +50 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +342 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +75 -0
  25. data/lib/features/steps/payload_steps.rb +234 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +107 -0
  29. data/lib/features/steps/runner_steps.rb +406 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/value_steps.rb +119 -0
  32. data/lib/features/support/env.rb +7 -0
  33. data/lib/features/support/internal_hooks.rb +260 -0
  34. data/lib/maze/appium_server.rb +112 -0
  35. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  36. data/lib/maze/aws/sam.rb +112 -0
  37. data/lib/maze/bitbar_devices.rb +84 -0
  38. data/lib/maze/bitbar_utils.rb +112 -0
  39. data/lib/maze/browser_stack_devices.rb +160 -0
  40. data/lib/maze/browser_stack_utils.rb +164 -0
  41. data/lib/maze/browsers_bs.yml +220 -0
  42. data/lib/maze/browsers_cbt.yml +100 -0
  43. data/lib/maze/bugsnag_config.rb +42 -0
  44. data/lib/maze/capabilities.rb +126 -0
  45. data/lib/maze/checks/assert_check.rb +91 -0
  46. data/lib/maze/checks/noop_check.rb +34 -0
  47. data/lib/maze/compare.rb +161 -0
  48. data/lib/maze/configuration.rb +174 -0
  49. data/lib/maze/docker.rb +108 -0
  50. data/lib/maze/document_server.rb +46 -0
  51. data/lib/maze/driver/appium.rb +217 -0
  52. data/lib/maze/driver/browser.rb +138 -0
  53. data/lib/maze/driver/resilient_appium.rb +51 -0
  54. data/lib/maze/errors.rb +20 -0
  55. data/lib/maze/helper.rb +118 -0
  56. data/lib/maze/hooks/appium_hooks.rb +216 -0
  57. data/lib/maze/hooks/browser_hooks.rb +68 -0
  58. data/lib/maze/hooks/command_hooks.rb +9 -0
  59. data/lib/maze/hooks/hooks.rb +61 -0
  60. data/lib/maze/interactive_cli.rb +173 -0
  61. data/lib/maze/logger.rb +73 -0
  62. data/lib/maze/macos_utils.rb +14 -0
  63. data/lib/maze/network.rb +49 -0
  64. data/lib/maze/option/parser.rb +245 -0
  65. data/lib/maze/option/processor.rb +143 -0
  66. data/lib/maze/option/validator.rb +184 -0
  67. data/lib/maze/option.rb +64 -0
  68. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  69. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  70. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  71. data/lib/maze/proxy.rb +114 -0
  72. data/lib/maze/request_list.rb +82 -0
  73. data/lib/maze/retry_handler.rb +76 -0
  74. data/lib/maze/runner.rb +149 -0
  75. data/lib/maze/sauce_labs_utils.rb +96 -0
  76. data/lib/maze/server.rb +207 -0
  77. data/lib/maze/servlets/base_servlet.rb +22 -0
  78. data/lib/maze/servlets/command_servlet.rb +44 -0
  79. data/lib/maze/servlets/log_servlet.rb +64 -0
  80. data/lib/maze/servlets/reflective_servlet.rb +69 -0
  81. data/lib/maze/servlets/servlet.rb +160 -0
  82. data/lib/maze/smart_bear_utils.rb +71 -0
  83. data/lib/maze/store.rb +15 -0
  84. data/lib/maze/terminating_server.rb +129 -0
  85. data/lib/maze/timers.rb +51 -0
  86. data/lib/maze/wait.rb +35 -0
  87. data/lib/maze.rb +27 -0
  88. metadata +371 -0
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative '../option'
5
+ require_relative '../browser_stack_devices'
6
+
7
+ module Maze
8
+ module Option
9
+ # Validates command line options
10
+ class Validator
11
+ # Validates all provided options
12
+ # @param options [Hash] Parsed command line options
13
+ def validate(options)
14
+ errors = []
15
+
16
+ # Common options
17
+ farm = options[Option::FARM]
18
+ if farm && !%w[bs cbt sl local bb].include?(farm)
19
+ errors << "--#{Option::FARM} must be 'bs', 'cbt', 'sl', 'bb' or 'local' if provided"
20
+ end
21
+
22
+ begin
23
+ JSON.parse(options[Option::CAPABILITIES])
24
+ rescue JSON::ParserError
25
+ errors << "--#{Option::CAPABILITIES} must be valid JSON (given #{options[Option::CAPABILITIES]})"
26
+ end
27
+
28
+ # Farm specific options
29
+ validate_bs options, errors if farm == 'bs'
30
+ validate_sl options, errors if farm == 'sl'
31
+ validate_bitbar options, errors if farm == 'bb'
32
+ validate_local options, errors if farm == 'local'
33
+
34
+ errors
35
+ end
36
+
37
+ # Validates BrowserStack options
38
+ def validate_bs(options, errors)
39
+ # BS local binary
40
+ bs_local = Maze::Helper.expand_path options[Option::BS_LOCAL]
41
+ errors << "BrowserStack local binary '#{bs_local}' not found" unless File.exist? bs_local
42
+
43
+ # Device
44
+ browser = options[Option::BROWSER]
45
+ device = options[Option::DEVICE]
46
+ if browser.nil? && device.empty?
47
+ errors << "Either --#{Option::BROWSER} or --#{Option::DEVICE} must be specified"
48
+ elsif browser
49
+
50
+ browsers = YAML.safe_load(File.read("#{__dir__}/../browsers_bs.yml"))
51
+
52
+ unless browsers.include? browser
53
+ browser_list = browsers.keys.join ', '
54
+ errors << "Browser type '#{browser}' unknown on BrowserStack. Must be one of: #{browser_list}."
55
+ end
56
+ elsif device
57
+ device.each do |device_key|
58
+ next if Maze::BrowserStackDevices::DEVICE_HASH.key? device_key
59
+ errors << "Device type '#{device_key}' unknown on BrowserStack. Must be one of #{Maze::BrowserStackDevices::DEVICE_HASH.keys}"
60
+ end
61
+ # App
62
+ app = Maze::Helper.read_at_arg_file options[Option::APP]
63
+ if app.nil?
64
+ errors << "--#{Option::APP} must be provided when running on a device"
65
+ else
66
+ # TODO: What about Sauce Labs URLs?
67
+ unless app.start_with?('bs://')
68
+ app = Maze::Helper.expand_path app
69
+ errors << "app file '#{app}' not found" unless File.exist?(app)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Credentials
75
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
76
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
77
+ end
78
+
79
+ # Validates Sauce Labs options
80
+ def validate_sl(options, errors)
81
+ # SL local binary
82
+ sl_local = Maze::Helper.expand_path options[Option::SL_LOCAL]
83
+ errors << "Sauce Connect binary '#{sl_local}' not found" unless File.exist? sl_local
84
+
85
+ # Device
86
+ browser = options[Option::BROWSER]
87
+ device = options[Option::DEVICE]
88
+ os = options[Option::OS]
89
+ os_version = options[Option::OS_VERSION]
90
+ if browser.nil? && device.nil? && os.nil? && os_version.nil?
91
+ errors << 'A device or browser option must be specified'
92
+ elsif browser
93
+ errors << 'Browsers not yet implemented on Sauce Labs'
94
+ else
95
+ # App
96
+ app = options[Option::APP]
97
+ if app.nil?
98
+ errors << "--#{Option::APP} must be provided when running on a device"
99
+ else
100
+ uuid_regex = /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/
101
+ unless uuid_regex.match? app
102
+ app = Maze::Helper.expand_path app
103
+ errors << "app file '#{app}' not found" unless File.exist?(app)
104
+ end
105
+ end
106
+
107
+ # OS
108
+ if options[Option::OS].nil?
109
+ errors << "--#{Option::OS} must be specified"
110
+ else
111
+ os = options[Option::OS].downcase
112
+ errors << 'os must be android or ios' unless %w[android ios].include? os
113
+ end
114
+
115
+ # OS Version
116
+ if options[Option::OS_VERSION].nil?
117
+ errors << "--#{Option::OS_VERSION} must be specified"
118
+ else
119
+ # Ensure OS version is a valid float so that notifier tests can perform numeric checks
120
+ # e.g 'Maze.config.os_version > 7'
121
+ unless /^[1-9][0-9]*(\.[0-9])?/.match? options[Option::OS_VERSION]
122
+ errors << "--#{Option::OS_VERSION} must be a valid version matching '/^[1-9][0-9]*(\\.[0-9])?/'"
123
+ end
124
+ end
125
+ end
126
+
127
+ # Credentials
128
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
129
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
130
+ end
131
+
132
+ # Validates BitBar device options
133
+ def validate_bitbar(options, errors)
134
+ if ENV['BUILDKITE']
135
+ errors << "--#{Option::TMS_URI} must be specified when running on Buildkite" if options[Option::TMS_URI].nil?
136
+ else
137
+ errors << "--#{Option::USERNAME} must be specified" if options[Option::USERNAME].nil?
138
+ errors << "--#{Option::ACCESS_KEY} must be specified" if options[Option::ACCESS_KEY].nil?
139
+ end
140
+
141
+ app = options[Option::APP]
142
+ if app.nil?
143
+ errors << "--#{Option::APP} must be provided when running on a device"
144
+ else
145
+ uuid_regex = /\A[0-9]+\z/
146
+ unless uuid_regex.match? app
147
+ app = Maze::Helper.expand_path app
148
+ errors << "app file '#{app}' not found" unless File.exist?(app)
149
+ end
150
+ end
151
+ end
152
+
153
+ # Validates Local device options
154
+ def validate_local(options, errors)
155
+ if options[Option::BROWSER].nil?
156
+ errors << "--#{Option::APP} must be specified" if options[Option::APP].nil?
157
+
158
+ # OS
159
+ if options[Option::OS].nil?
160
+ errors << "--#{Option::OS} must be specified"
161
+ else
162
+ os = options[Option::OS].downcase
163
+ errors << 'os must be android, ios, macos or windows' unless %w[android ios macos windows].include? os
164
+ if os == 'ios'
165
+ errors << "--#{Option::APPLE_TEAM_ID} must be specified for iOS" if options[Option::APPLE_TEAM_ID].nil?
166
+ errors << "--#{Option::UDID} must be specified for iOS" if options[Option::UDID].nil?
167
+ end
168
+ end
169
+
170
+ # OS Version
171
+ unless options[Option::OS_VERSION].nil?
172
+ # Ensure OS version is a valid float so that notifier tests can perform numeric checks
173
+ # e.g 'Maze.config.os_version > 7'
174
+ unless /^[1-9][0-9]*(\.[0-9])?/.match? options[Option::OS_VERSION]
175
+ errors << "--#{Option::OS_VERSION} must be a valid version matching '/^[1-9][0-9]*(\\.[0-9])?/'"
176
+ end
177
+ end
178
+ else
179
+ # TODO Validate browser options
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides the set of Maze Runner command line options
4
+ module Maze
5
+ module Option
6
+ # Document server options
7
+ DS_BIND_ADDRESS = 'document-server-bind-address'
8
+ DS_PORT = 'document-server-port'
9
+ DS_ROOT = 'document-server-root'
10
+
11
+ # Server options
12
+ BIND_ADDRESS = 'bind-address'
13
+ PORT = 'port'
14
+ NULL_PORT = 'null-port'
15
+
16
+ # Appium options
17
+ SEPARATE_SESSIONS = 'separate-sessions'
18
+ FARM = 'farm'
19
+ APP = 'app'
20
+ A11Y_LOCATOR = 'a11y-locator'
21
+ RESILIENT = 'resilient'
22
+ CAPABILITIES = 'capabilities'
23
+
24
+ # Generic device farm options
25
+ USERNAME = 'username'
26
+ ACCESS_KEY = 'access-key'
27
+ APPIUM_VERSION = 'appium-version'
28
+ DEVICE = 'device'
29
+ BROWSER = 'browser'
30
+ OS = 'os'
31
+ OS_VERSION = 'os-version'
32
+ LIST_DEVICES = 'list-devices'
33
+ APP_BUNDLE_ID = 'app-bundle-id'
34
+
35
+ # CrossBrowserTesting/Bitbar options
36
+ SB_LOCAL = 'sb-local'
37
+
38
+ # BrowserStack-only options
39
+ BS_LOCAL = 'bs-local'
40
+
41
+ # Sauce Labs-only options
42
+ SL_LOCAL = 'sl-local'
43
+
44
+ # BitBar-only options
45
+ TMS_URI = 'tms-uri'
46
+ TMS_TOKEN = 'tms-token'
47
+
48
+ # Local-only options
49
+ APPIUM_SERVER = 'appium-server'
50
+ START_APPIUM = 'start-appium'
51
+ APPIUM_LOGFILE = 'appium-logfile'
52
+ APPLE_TEAM_ID = 'apple-team-id'
53
+ UDID = 'udid'
54
+
55
+ # Logging options
56
+ FILE_LOG = 'file-log'
57
+ LOG_REQUESTS = 'log-requests'
58
+ ALWAYS_LOG = 'always-log'
59
+
60
+ # Runtime options
61
+ ENABLE_RETRIES = 'enable-retries'
62
+ ENABLE_BUGSNAG = 'enable-bugsnag'
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugsnag'
4
+ require 'cucumber/core/filter'
5
+
6
+ # Required to access the options
7
+ module Cucumber
8
+ class Configuration
9
+ attr_accessor :options
10
+ end
11
+ end
12
+
13
+ module Maze
14
+ module Plugins
15
+ class BugsnagReportingPlugin < Cucumber::Core::Filter.new(:configuration)
16
+
17
+ def test_case(test_case)
18
+ configuration.on_event(:test_step_finished) do |event|
19
+ @last_test_step = event.test_step if event.result.failed?
20
+ end
21
+
22
+ configuration.on_event(:test_case_finished) do |event|
23
+
24
+ # Ensure we're in the correct test case and that it's failed
25
+ next unless event.test_case.eql?(test_case) && event.result.failed?
26
+
27
+ Bugsnag.notify(event.result.exception) do |bsg_event|
28
+ unless @last_test_step.nil?
29
+ bsg_event.context = @last_test_step.location
30
+ bsg_event.grouping_hash = test_case.name + @last_test_step.location
31
+ bsg_event.add_metadata(:'scenario', {
32
+ 'failing step': @last_test_step.to_s,
33
+ 'failing step location': @last_test_step.location
34
+ })
35
+ end
36
+ bsg_event.add_metadata(:'scenario', {
37
+ 'scenario name': test_case.name,
38
+ 'scenario location': test_case.location,
39
+ 'scenario tags': test_case.tags,
40
+ 'scenario duration (mS)': event.result.duration.nanoseconds/1000000
41
+ })
42
+ end
43
+ end
44
+
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugsnag'
4
+ require 'cucumber/core/filter'
5
+ require 'json'
6
+
7
+ # Required to access the options
8
+ module Cucumber
9
+ class Configuration
10
+ attr_accessor :options
11
+ end
12
+ end
13
+
14
+ module Maze
15
+ module Plugins
16
+ class CucumberReportPlugin
17
+
18
+ def initialize
19
+ configured_data = {
20
+ driver_class: Maze.driver.class,
21
+ device_farm: Maze.config.farm,
22
+ device: Maze.config.device,
23
+ os: Maze.config.os,
24
+ os_version: Maze.config.os_version
25
+ }
26
+ buildkite_data = {
27
+ pipeline: ENV['BUILDKITE_PIPELINE_NAME'],
28
+ repo: ENV['BUILDKITE_REPO'],
29
+ build_url: ENV['BUILDKITE_BUILD_URL'],
30
+ branch: ENV['BUILDKITE_BRANCH'],
31
+ message: ENV['BUILDKITE_MESSAGE'],
32
+ step: ENV['BUILDKITE_LABEL'],
33
+ commit: ENV['BUILDKITE_COMMIT']
34
+ }
35
+ report['configuration'] = configured_data
36
+ report['build'] = buildkite_data
37
+ end
38
+
39
+ def install_plugin(cuc_config)
40
+ unless Maze.config.tms_uri && Maze.config.tms_token && ENV['BUILDKITE']
41
+ $logger.info 'No test report will be delivered for this run'
42
+ return
43
+ end
44
+ # Add installation hook
45
+ cuc_config.formats << ['json', {}, json_report_stream]
46
+
47
+ # Add exit hook
48
+ at_exit do
49
+ finish_report
50
+ end
51
+ end
52
+
53
+ def json_report_stream
54
+ @json_report_stream ||= StringIO.new
55
+ end
56
+
57
+ def report
58
+ @report ||= {}
59
+ end
60
+
61
+ private
62
+
63
+ def finish_report
64
+ session_hash = JSON.parse(json_report_stream.string)
65
+ report[:session] = session_hash
66
+ output_folder = File.join(Dir.pwd, 'maze_output')
67
+ filename = 'maze_report.json'
68
+ filepath = File.join(output_folder, filename)
69
+
70
+ begin
71
+ File.open(filepath, 'w') do |file|
72
+ file.puts JSON.pretty_generate(report)
73
+ end
74
+ rescue => e
75
+ $logger.warn 'Report could not be saved locally'
76
+ $logger.warn e.message
77
+ end
78
+
79
+ send_report
80
+ end
81
+
82
+ def send_report
83
+ uri = URI("#{Maze.config.tms_uri}/report")
84
+ request = Net::HTTP::Post.new(uri)
85
+ request['Content-Type'] = 'application/json'
86
+ request['Authorization'] = Maze.config.tms_token
87
+ request.body = JSON.generate(report)
88
+
89
+ begin
90
+ http = Net::HTTP.new(uri.hostname, uri.port)
91
+ response = http.request(request)
92
+ rescue => e
93
+ $logger.warn 'Report delivery attempt failed'
94
+ $logger.warn e.message
95
+ else
96
+ $logger.info 'Cucumber report delivered to test report server'
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/core/filter'
4
+
5
+ # Required to access the options
6
+ module Cucumber
7
+ class Configuration
8
+ attr_accessor :options
9
+ end
10
+ end
11
+
12
+ module Maze
13
+ module Plugins
14
+ class GlobalRetryPlugin < Cucumber::Core::Filter.new(:configuration)
15
+
16
+ def test_case(test_case)
17
+ configuration.on_event(:test_case_finished) do |event|
18
+
19
+ # Ensure we're in the correct test case
20
+ next unless event.test_case == test_case
21
+
22
+ # Set retry to 0
23
+ configuration.options[:retry] = 0
24
+
25
+ # Guard to check if the case should be retried
26
+ should_retry = event.result.failed? && Maze::RetryHandler.should_retry?(test_case, event)
27
+
28
+ next unless should_retry
29
+
30
+ # Set retry to 1
31
+ configuration.options[:retry] = 1
32
+ end
33
+
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/maze/proxy.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'singleton'
5
+ require 'webrick'
6
+ require 'webrick/https'
7
+ require 'webrick/httpproxy'
8
+
9
+ module Maze
10
+ # Provides the ability to run a proxy server, using the WEBrick proxy server.
11
+ # Note that for an HTTPS proxy a self-signed certificate will be used. If using curl, for example, this
12
+ # means having to employ the --proxy-insecure option.
13
+ class Proxy
14
+ include Singleton
15
+
16
+ # There are some constraints on the port from driving remote browsers on BrowserStack.
17
+ # E.g. the ports/ranges that Safari will access on "localhost" urls are restricted to the following:
18
+ # 80, 3000, 4000, 5000, 8000, 8080 or 9000-9999 [ from https://stackoverflow.com/a/28678652 ]
19
+ PORT = 9000
20
+
21
+ def initialize
22
+ @hosts = []
23
+ end
24
+
25
+ # Whether the proxy handled a request for the given host
26
+ #
27
+ # @param host [String] The destination host to test for
28
+ def handled_host?(host)
29
+ @hosts.include? host
30
+ end
31
+
32
+ # Whether the proxy server thread is running
33
+ #
34
+ def running?
35
+ @thread&.alive?
36
+ end
37
+
38
+ # Starts the WEBrick proxy in a separate thread
39
+ # If authentication if requested, then the credentials used are simply 'user' with 'password'.
40
+ #
41
+ # @param protocol [Symbol] :Http or Https
42
+ # @param authenticated [Boolean] Whether basic authentication should be applied.
43
+ def start(protocol, authenticated = false)
44
+ @hosts.clear
45
+
46
+ attempts = 0
47
+ $logger.info 'Starting proxy server'
48
+ loop do
49
+ @thread = Thread.new do
50
+
51
+ handler = proc do |req, res|
52
+ req.header['host'].each { |host| @hosts.append(host) }
53
+ end
54
+ config = {
55
+ Logger: $logger,
56
+ Port: PORT,
57
+ ProxyContentHandler: handler
58
+ }
59
+
60
+ # Setup protocol
61
+ if protocol == :Http
62
+ $logger.info 'Starting HTTP proxy'
63
+ elsif protocol == :Https
64
+ $logger.info 'Starting HTTPS proxy'
65
+ cert_name = [
66
+ %w[CN localhost]
67
+ ]
68
+ config[:SSLCertName] = cert_name
69
+ config[:SSLEnable] = true
70
+ else
71
+ raise "Unsupported protocol #{protocol}: :Http and :Https are supported"
72
+ end
73
+
74
+ # Authentication required?
75
+ if authenticated
76
+ # Apache compatible Password manager
77
+ htpasswd = WEBrick::HTTPAuth::Htpasswd.new File.expand_path('htpasswd', __dir__)
78
+ htpasswd.set_passwd 'Proxy Realm', 'user', 'password'
79
+ htpasswd.flush
80
+ authenticator = WEBrick::HTTPAuth::ProxyBasicAuth.new Realm: 'Proxy Realm',
81
+ UserDB: htpasswd
82
+ config[:ProxyAuthProc] = authenticator.method(:authenticate).to_proc
83
+ end
84
+
85
+ # Crwate and start the proxy
86
+ proxy = WEBrick::HTTPProxyServer.new config
87
+ proxy.start
88
+ rescue StandardError => e
89
+ $logger.warn "Failed to start proxy server: #{e.message}"
90
+ ensure
91
+ proxy&.shutdown
92
+ end
93
+
94
+ # Need a short sleep here as a dying thread is still alive momentarily
95
+ sleep 1
96
+ break if running?
97
+
98
+ # Bail out after 3 attempts
99
+ attempts += 1
100
+ raise 'Too many failed attempts to start proxy server' if attempts == 3
101
+
102
+ # Failed to start - sleep before retrying
103
+ $logger.info 'Retrying in 5 seconds'
104
+ sleep 5
105
+ end
106
+ end
107
+
108
+ # Stops the WEBrick proxy thread if it's running
109
+ def stop
110
+ @thread&.kill if @thread&.alive?
111
+ @thread = nil
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # An abstraction for storing a list of requests (e.g. Errors, Sessions),
5
+ # keeping track of the "current" request (i.e. the one being inspected).
6
+ class RequestList
7
+ def initialize
8
+ @requests = []
9
+ @current = 0
10
+ @count = 0
11
+ end
12
+
13
+ def empty?
14
+ @requests.empty?
15
+ end
16
+
17
+ # The number of unprocessed/remaining requests in the list (not the total number actually held)
18
+ def size
19
+ @count
20
+ end
21
+
22
+ # The total number of requests received, including those already processed
23
+ def size_all
24
+ @requests.size
25
+ end
26
+
27
+ # Add a request to the list
28
+ #
29
+ # @param request The new request, from which a clone is made
30
+ def add(request)
31
+ @requests.append request.clone
32
+ @count += 1
33
+ end
34
+
35
+ # The current request
36
+ def current
37
+ @requests[@current] if @requests.size > @current
38
+ end
39
+
40
+ # Peek at requests yet to be processed - i.e. from current onwards. All requests are left visible in the list.
41
+ # Returns an empty array if there are no requests outstanding.
42
+ def remaining
43
+ return [] if current.nil?
44
+
45
+ @requests[@current..@requests.size]
46
+ end
47
+
48
+ # Moves to the next request, if there is one
49
+ def next
50
+ return if @current >= @requests.size
51
+
52
+ @current += 1
53
+ @count -= 1
54
+ end
55
+
56
+ # A frozen clone of all requests held, including those already processed
57
+ def all
58
+ @requests.clone.freeze
59
+ end
60
+
61
+ # Clears the list
62
+ def clear
63
+ @requests.clear
64
+ @current = 0
65
+ @count = 0
66
+ end
67
+
68
+ # Sorts the first `count` elements of the list by the Bugsnag-Sent-At header, if present in all of those elements
69
+ def sort_by_sent_at!(count)
70
+ return unless count > 1
71
+
72
+ header = 'Bugsnag-Sent-At'
73
+ sub_list = @requests[@current...@current + count]
74
+
75
+ return if sub_list.any? { |r| r[:request][header].nil? }
76
+
77
+ # Sort sublist by Bugsnag-Sent-At and overwrite in the main list
78
+ sub_list.sort_by! { |r| DateTime.parse(r[:request][header]) }
79
+ sub_list.each_with_index { |r, i| @requests[@current + i] = r }
80
+ end
81
+ end
82
+ end