Class: HttpDecoy::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/http_decoy/server.rb

Overview

A real WEBrick HTTP server that runs in a background thread.

Uses WEBrick directly (no Rack::Handler) so it works with both Rack 2.x and Rack 3.x without the rackup gem.

Port 0 lets the OS pick a free port atomically — parallel test runners never collide.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(route_map) ⇒ Server

Returns a new instance of Server.



22
23
24
25
26
27
28
29
30
# File 'lib/http_decoy/server.rb', line 22

def initialize(route_map)
  @route_map   = route_map
  @request_log = RequestLog.new
  @webrick     = nil
  @thread      = nil
  @port        = nil
  @scenario    = nil
  @scenario_mu = Mutex.new
end

Instance Attribute Details

#portObject (readonly)

Returns the value of attribute port.



20
21
22
# File 'lib/http_decoy/server.rb', line 20

def port
  @port
end

#request_logObject (readonly)

Returns the value of attribute request_log.



20
21
22
# File 'lib/http_decoy/server.rb', line 20

def request_log
  @request_log
end

#route_mapObject (readonly)

Returns the value of attribute route_map.



20
21
22
# File 'lib/http_decoy/server.rb', line 20

def route_map
  @route_map
end

Instance Method Details

#base_urlObject



79
# File 'lib/http_decoy/server.rb', line 79

def base_url = "http://127.0.0.1:#{@port}"

#current_scenarioObject



93
94
95
# File 'lib/http_decoy/server.rb', line 93

def current_scenario
  @scenario_mu.synchronize { @scenario }
end

#rack_appObject

The Rack app is also exposed for WebMock's to_rack() interception.



82
83
84
# File 'lib/http_decoy/server.rb', line 82

def rack_app
  @rack_app ||= build_rack_app
end

#startObject



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/http_decoy/server.rb', line 32

def start
  rack_app = build_rack_app

  # WEBrick binds to the OS-assigned port during initialization.
  @webrick = WEBrick::HTTPServer.new(
    Port: 0,
    Logger: WEBrick::Log.new(File::NULL),
    AccessLog: []
  )
  @port = @webrick.config[:Port]

  @webrick.mount_proc("/") do |req, res|
    status, headers, body = rack_app.call(rack_env_from(req))
    res.status = status.to_i
    headers.each { |k, v| res[k] = v }
    res.body = Array(body).join
  rescue Timeout::Error, Errno::ECONNRESET, Errno::ECONNREFUSED
    # raise_error(:timeout/:reset/:refused): over a real socket there's no
    # response that means "the connection died" — the only faithful
    # simulation is to actually kill the connection. (Over WebMock's
    # to_rack interception this same exception instead propagates directly
    # to the caller, which is the more common path and needs no help here.)
    drop_connection(req)
  rescue StandardError => e
    res.status = 500
    res["Content-Type"] = "application/json"
    res.body = JSON.generate(error: "#{e.class}: #{e.message}")
  end

  @thread = Thread.new { @webrick.start }
  @thread.abort_on_exception = true

  # Poll until WEBrick enters its accept loop.
  deadline = Time.now + 5
  sleep(0.005) until @webrick.status == :Running || Time.now > deadline
  raise "http_decoy: server failed to start within 5 seconds" unless @webrick.status == :Running

  self
end

#stopObject



72
73
74
75
76
77
# File 'lib/http_decoy/server.rb', line 72

def stop
  @webrick&.shutdown
  @thread&.join(3)
  @thread  = nil
  @webrick = nil
end

#with_scenario(name) ⇒ Object



86
87
88
89
90
91
# File 'lib/http_decoy/server.rb', line 86

def with_scenario(name)
  @scenario_mu.synchronize { @scenario = name }
  yield
ensure
  @scenario_mu.synchronize { @scenario = nil }
end