itsi 0.1.8 → 0.1.11

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +11 -2
  3. data/Rakefile +6 -2
  4. data/crates/itsi_rb_helpers/src/lib.rs +27 -4
  5. data/crates/itsi_server/Cargo.toml +4 -1
  6. data/crates/itsi_server/src/lib.rs +74 -1
  7. data/crates/itsi_server/src/request/itsi_request.rs +32 -11
  8. data/crates/itsi_server/src/response/itsi_response.rs +14 -4
  9. data/crates/itsi_server/src/server/bind.rs +16 -12
  10. data/crates/itsi_server/src/server/itsi_server.rs +146 -95
  11. data/crates/itsi_server/src/server/listener.rs +10 -10
  12. data/crates/itsi_server/src/server/process_worker.rs +10 -3
  13. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  14. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
  15. data/crates/itsi_server/src/server/signal.rs +4 -0
  16. data/crates/itsi_server/src/server/thread_worker.rs +55 -24
  17. data/crates/itsi_server/src/server/tls.rs +11 -8
  18. data/crates/itsi_tracing/src/lib.rs +18 -1
  19. data/gems/scheduler/Cargo.lock +12 -12
  20. data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +27 -4
  21. data/gems/scheduler/ext/itsi_server/Cargo.toml +4 -1
  22. data/gems/scheduler/ext/itsi_server/src/lib.rs +74 -1
  23. data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +32 -11
  24. data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +14 -4
  25. data/gems/scheduler/ext/itsi_server/src/server/bind.rs +16 -12
  26. data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +146 -95
  27. data/gems/scheduler/ext/itsi_server/src/server/listener.rs +10 -10
  28. data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +10 -3
  29. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  30. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
  31. data/gems/scheduler/ext/itsi_server/src/server/signal.rs +4 -0
  32. data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +55 -24
  33. data/gems/scheduler/ext/itsi_server/src/server/tls.rs +11 -8
  34. data/gems/scheduler/ext/itsi_tracing/src/lib.rs +18 -1
  35. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  36. data/gems/scheduler/test/test_address_resolve.rb +0 -1
  37. data/gems/scheduler/test/test_file_io.rb +0 -1
  38. data/gems/scheduler/test/test_kernel_sleep.rb +3 -4
  39. data/gems/server/Cargo.lock +11 -2
  40. data/gems/server/Rakefile +8 -1
  41. data/gems/server/exe/itsi +53 -23
  42. data/gems/server/ext/itsi_rb_helpers/src/lib.rs +27 -4
  43. data/gems/server/ext/itsi_server/Cargo.toml +4 -1
  44. data/gems/server/ext/itsi_server/src/lib.rs +74 -1
  45. data/gems/server/ext/itsi_server/src/request/itsi_request.rs +32 -11
  46. data/gems/server/ext/itsi_server/src/response/itsi_response.rs +14 -4
  47. data/gems/server/ext/itsi_server/src/server/bind.rs +16 -12
  48. data/gems/server/ext/itsi_server/src/server/itsi_server.rs +146 -95
  49. data/gems/server/ext/itsi_server/src/server/listener.rs +10 -10
  50. data/gems/server/ext/itsi_server/src/server/process_worker.rs +10 -3
  51. data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  52. data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
  53. data/gems/server/ext/itsi_server/src/server/signal.rs +4 -0
  54. data/gems/server/ext/itsi_server/src/server/thread_worker.rs +55 -24
  55. data/gems/server/ext/itsi_server/src/server/tls.rs +11 -8
  56. data/gems/server/ext/itsi_tracing/src/lib.rs +18 -1
  57. data/gems/server/lib/itsi/request.rb +29 -21
  58. data/gems/server/lib/itsi/server/Itsi.rb +127 -0
  59. data/gems/server/lib/itsi/server/config.rb +36 -0
  60. data/gems/server/lib/itsi/server/options_dsl.rb +401 -0
  61. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +18 -7
  62. data/gems/server/lib/itsi/server/rack_interface.rb +75 -0
  63. data/gems/server/lib/itsi/server/scheduler_interface.rb +21 -0
  64. data/gems/server/lib/itsi/server/signal_trap.rb +23 -0
  65. data/gems/server/lib/itsi/server/version.rb +1 -1
  66. data/gems/server/lib/itsi/server.rb +71 -101
  67. data/gems/server/test/helpers/test_helper.rb +30 -0
  68. data/gems/server/test/test_itsi_server.rb +294 -3
  69. data/lib/itsi/version.rb +1 -1
  70. data/location_dsl.rb +381 -0
  71. data/sandbox/deploy/main.tf +1 -0
  72. data/sandbox/itsi_itsi_file/Itsi.rb +119 -0
  73. data/sandbox/itsi_sandbox_async/Gemfile +1 -1
  74. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  75. data/sandbox/itsi_sandbox_rails/Gemfile.lock +2 -2
  76. data/tasks.txt +25 -8
  77. metadata +21 -14
  78. data/gems/server/lib/itsi/signals.rb +0 -23
  79. data/gems/server/test/test_helper.rb +0 -7
  80. /data/gems/server/lib/itsi/{index.html.erb → index.html} +0 -0
@@ -0,0 +1,75 @@
1
+ module Itsi
2
+ class Server
3
+ module RackInterface
4
+ # Interface to Rack applications.
5
+ # Here we build the env, and invoke the Rack app's call method.
6
+ # We then turn the Rack response into something Itsi server understands.
7
+ def call(app, request)
8
+ respond request, app.call(request.to_rack_env)
9
+ end
10
+
11
+ # Itsi responses are asynchronous and can be streamed.
12
+ # Response chunks are sent using response.send_frame
13
+ # and the response is finished using response.close_write.
14
+ # If only a single chunk is written, you can use the #send_and_close method.
15
+ def respond(request, (status, headers, body))
16
+ response = request.response
17
+
18
+ # Don't try and respond if we've been hijacked.
19
+ # The hijacker is now responsible for this.
20
+ return if request.hijacked
21
+
22
+ # 1. Set Status
23
+ response.status = status
24
+
25
+ # 2. Set Headers
26
+ body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
27
+ headers.each do |key, value|
28
+ next response.add_header(key, value) unless value.is_a?(Array)
29
+
30
+ value.each do |v|
31
+ response.add_header(key, v)
32
+ end
33
+ end
34
+
35
+ # 3. Set Body
36
+ # As soon as we start setting the response
37
+ # the server will begin to stream it to the client.
38
+
39
+ # If we're partially hijacked or returned a streaming body,
40
+ # stream this response.
41
+
42
+ if body_streamer
43
+ body_streamer.call(StreamIO.new(response))
44
+
45
+ # If we're enumerable with more than one chunk
46
+ # also stream, otherwise write in a single chunk
47
+ elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
48
+ unless body.respond_to?(:each)
49
+ body = body.to_ary
50
+ raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
51
+ end
52
+ # We offset this iteration intentionally,
53
+ # to optimize for the case where there's only one chunk.
54
+ buffer = nil
55
+ body.each do |part|
56
+ response.send_frame(buffer.to_s) if buffer
57
+ buffer = part
58
+ end
59
+
60
+ response.send_and_close(buffer.to_s)
61
+ else
62
+ response.send_and_close(body.to_s)
63
+ end
64
+ ensure
65
+ response.close_write
66
+ body.close if body.respond_to?(:close)
67
+ end
68
+
69
+ # A streaming body is one that responds to #call and not #each.
70
+ def streaming_body?(body)
71
+ body.respond_to?(:call) && !body.respond_to?(:each)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,21 @@
1
+ module Itsi
2
+ class Server
3
+ module SchedulerInterface
4
+ # Simple wrapper to instantiate a scheduler, start it,
5
+ # and immediate have it invoke a scheduler proc
6
+ def start_scheduler_loop(scheduler_class, scheduler_task)
7
+ scheduler = scheduler_class.new
8
+ Fiber.set_scheduler(scheduler)
9
+ [scheduler, Fiber.schedule(&scheduler_task)]
10
+ end
11
+
12
+ # When running in scheduler mode,
13
+ # each request is wrapped in a Fiber.
14
+ def schedule(app, request)
15
+ Fiber.schedule do
16
+ call(app, request)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Itsi
2
+ module SignalTrap
3
+
4
+ DEFAULT_SIGNALS = ["DEFAULT", "", nil].freeze
5
+ INTERCEPTED_SIGNALS = ["INT"].freeze
6
+
7
+ def trap(signal, *args, &block)
8
+ unless INTERCEPTED_SIGNALS.include?(signal.to_s) && block.nil? && Itsi::Server.running?
9
+ return super(signal, *args, &block)
10
+ end
11
+ Itsi::Server.reset_signal_handlers
12
+ nil
13
+ end
14
+ end
15
+ end
16
+
17
+ [Kernel, Signal].each do |receiver|
18
+ receiver.singleton_class.prepend(Itsi::SignalTrap)
19
+ end
20
+
21
+ [Object].each do |receiver|
22
+ receiver.include(Itsi::SignalTrap)
23
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.8"
5
+ VERSION = "0.1.11"
6
6
  end
7
7
  end
@@ -2,119 +2,89 @@
2
2
 
3
3
  require_relative "server/version"
4
4
  require_relative "server/itsi_server"
5
- require_relative "signals"
5
+ require_relative "server/rack_interface"
6
+ require_relative "server/signal_trap"
7
+ require_relative "server/scheduler_interface"
8
+ require_relative "server/rack/handler/itsi"
9
+ require_relative "server/config"
6
10
  require_relative "request"
7
11
  require_relative "stream_io"
8
- require_relative "server/rack/handler/itsi"
9
- require 'erb'
10
12
 
11
- DEFAULT_INDEX = IO.read(__dir__ + '/index.html.erb')
13
+ # When you Run Itsi without a Rack app,
14
+ # we start a tiny little echo server, just so you can see it in action.
15
+ DEFAULT_INDEX = IO.read("#{__dir__}/index.html").freeze
16
+ DEFAULT_BINDS = ["http://0.0.0.0:3000"].freeze
17
+ DEFAULT_APP = lambda {
18
+ require "json"
19
+ require "itsi/scheduler"
20
+ Itsi.log_warn "No config.ru or Itsi.rb app detected. Running default app."
21
+ lambda do |env|
22
+ headers, body = \
23
+ if env["itsi.response"].json?
24
+ [
25
+ { "Content-Type" => "application/json" },
26
+ [{ "message" => "You're running on Itsi!", "rack_env" => env,
27
+ "version" => Itsi::Server::VERSION }.to_json]
28
+ ]
29
+ else
30
+ [
31
+ { "Content-Type" => "text/html" },
32
+ [
33
+ format(
34
+ DEFAULT_INDEX,
35
+ REQUEST_METHOD: env["REQUEST_METHOD"],
36
+ PATH_INFO: env["PATH_INFO"],
37
+ SERVER_NAME: env["SERVER_NAME"],
38
+ SERVER_PORT: env["SERVER_PORT"],
39
+ REMOTE_ADDR: env["REMOTE_ADDR"],
40
+ HTTP_USER_AGENT: env["HTTP_USER_AGENT"]
41
+ )
42
+ ]
43
+ ]
44
+ end
45
+ [200, headers, body]
46
+ end
47
+ }
12
48
 
13
49
  module Itsi
14
50
  class Server
51
+ extend RackInterface
52
+ extend SchedulerInterface
15
53
 
16
- def self.running?
17
- @running ||= false
18
- end
19
-
20
- def self.start(
21
- app: ->(env){
22
- [env['CONTENT_TYPE'], env['HTTP_ACCEPT']].include?('application/json') ?
23
- [200, {"Content-Type" => "application/json"}, ["{\"message\": \"You're running on Itsi!\"}"]] :
24
- [200, {"Content-Type" => "text/html"}, [
25
- DEFAULT_INDEX % {
26
- REQUEST_METHOD: env['REQUEST_METHOD'],
27
- PATH_INFO: env['PATH_INFO'],
28
- SERVER_NAME: env['SERVER_NAME'],
29
- SERVER_PORT: env['SERVER_PORT'],
30
- REMOTE_ADDR: env['REMOTE_ADDR'],
31
- HTTP_USER_AGENT: env['HTTP_USER_AGENT']
32
- }
33
- ]]
34
- },
35
- binds: ['http://0.0.0.0:3000'],
36
- **opts
37
- )
38
- server = new(app: ->{app}, binds: binds, **opts)
39
- @running = true
40
- Signal.trap('INT', 'DEFAULT')
41
- server.start
42
- ensure
43
- @running = false
44
- end
45
-
46
- def self.call(app, request)
47
- respond request, app.call(request.to_env)
48
- end
49
-
50
- def self.streaming_body?(body)
51
- body.respond_to?(:call) && !body.respond_to?(:each)
52
- end
53
-
54
- def self.respond(request, (status, headers, body))
55
- response = request.response
56
-
57
- # Don't try and respond if we've been hijacked.
58
- # The hijacker is now responsible for this.
59
- return if request.hijacked
60
-
61
- # 1. Set Status
62
- response.status = status
63
-
64
- # 2. Set Headers
65
- headers.each do |key, value|
66
- next response.add_header(key, value) unless value.is_a?(Array)
67
-
68
- value.each do |v|
69
- response.add_header(key, v)
70
- end
54
+ class << self
55
+ def running?
56
+ !!@running
71
57
  end
72
58
 
73
- # 3. Set Body
74
- # As soon as we start setting the response
75
- # the server will begin to stream it to the client.
76
-
77
- # If we're partially hijacked or returned a streaming body,
78
- # stream this response.
79
-
80
- if (body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack"))
81
- body_streamer.call(StreamIO.new(response))
82
-
83
- # If we're enumerable with more than one chunk
84
- # also stream, otherwise write in a single chunk
85
- elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
86
- unless body.respond_to?(:each)
87
- body = body.to_ary
88
- raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
89
- end
90
- # We offset this iteration intentionally,
91
- # to optimize for the case where there's only one chunk.
92
- buffer = nil
93
- body.each do |part|
94
- response.send_frame(buffer.to_s) if buffer
95
- buffer = part
96
- end
97
-
98
- response.send_and_close(buffer.to_s)
99
- else
100
- response.send_and_close(body.to_s)
59
+ def build(
60
+ app: DEFAULT_APP[],
61
+ loader: nil,
62
+ binds: DEFAULT_BINDS,
63
+ **opts
64
+ )
65
+ new(app: loader || -> { app }, binds: binds, **opts)
101
66
  end
102
- ensure
103
- response.close_write
104
- body.close if body.respond_to?(:close)
105
- end
106
67
 
107
- def self.start_scheduler_loop(scheduler_class, scheduler_task)
108
- scheduler = scheduler_class.new
109
- Fiber.set_scheduler(scheduler)
110
- [scheduler, Fiber.schedule(&scheduler_task)]
111
- end
68
+ def start_in_background_thread(silence: true, **opts)
69
+ start(background: true, silence: silence, **opts)
70
+ end
112
71
 
113
- # If scheduler is enabled
114
- # Each request is wrapped in a Fiber.
115
- def self.schedule(app, request)
116
- Fiber.schedule do
117
- call(app, request)
72
+ def start(background: false, silence: false, **opts)
73
+ build(**opts).tap do |server|
74
+ previous_handler = Signal.trap("INT", "DEFAULT")
75
+ @running = true
76
+ if background
77
+ Thread.new do
78
+ server.start
79
+ @running = false
80
+ Signal.trap("INT", previous_handler)
81
+ end
82
+ else
83
+ server.start
84
+ @running = false
85
+ Signal.trap("INT", previous_handler)
86
+ end
87
+ end
118
88
  end
119
89
  end
120
90
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/reporters"
4
+
5
+ ENV['ITSI_LOG'] = 'off'
6
+
7
+ require "itsi/server"
8
+ require "itsi/scheduler"
9
+
10
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
11
+
12
+ def free_bind(protocol)
13
+ server = TCPServer.new("0.0.0.0", 0)
14
+ port = server.addr[1]
15
+ server.close
16
+ "#{protocol}://0.0.0.0:#{port}"
17
+ end
18
+
19
+ def run_app(app, protocol: "http", bind: free_bind(protocol), **opts)
20
+ server = Itsi::Server.start_in_background_thread(
21
+ app: app,
22
+ binds: [bind],
23
+ **opts
24
+ )
25
+
26
+ sleep 0.005
27
+ yield URI(bind), server
28
+ ensure
29
+ server&.stop
30
+ end
@@ -1,9 +1,300 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "test_helper"
1
+ require "socket"
2
+ require "net/http"
3
+ require "minitest/autorun"
4
4
 
5
5
  class TestItsiServer < Minitest::Test
6
6
  def test_that_it_has_a_version_number
7
7
  refute_nil ::Itsi::Server::VERSION
8
8
  end
9
+
10
+ def test_hello_world
11
+ run_app(lambda do |env|
12
+ [200, { "Content-Type" => "text/plain" }, ["Hello, World!"]]
13
+ end) do |uri|
14
+ assert_equal "Hello, World!", Net::HTTP.get(uri)
15
+ end
16
+ end
17
+
18
+ def test_post
19
+ run_app(lambda do |env|
20
+ assert_equal env["REQUEST_METHOD"], "POST"
21
+ assert_equal "data", env["rack.input"].read
22
+ [200, { "Content-Type" => "text/plain" }, ["Hello, World!"]]
23
+ end) do |uri|
24
+ assert_equal "Hello, World!", Net::HTTP.post(uri, "data").body
25
+ end
26
+ end
27
+
28
+ def test_full_hijack
29
+ run_app(lambda do |env|
30
+ io = env["rack.hijack"].call
31
+ io.write("HTTP/1.1 200 Ok\r\n")
32
+ io.write("Content-Type: text/plain\r\n")
33
+ io.write("Transfer-Encoding: chunked\r\n")
34
+ io.write("\r\n")
35
+ io.write("7\r\n")
36
+ io.write("Hello, \r\n")
37
+ io.write("6\r\n")
38
+ io.write("World!\r\n")
39
+ io.write("0\r\n\r\n")
40
+ io.close
41
+ end) do |uri|
42
+ assert_equal "Hello, World!", Net::HTTP.get(uri)
43
+ end
44
+ end
45
+
46
+ def test_streaming_body
47
+ run_app(lambda do |env|
48
+ [200, { "Content-Type" => "text/plain" }, lambda { |stream|
49
+ stream.write("Hello")
50
+ stream.write(", World!")
51
+ stream.close
52
+ }]
53
+ end) do |uri|
54
+ assert_equal "Hello, World!", Net::HTTP.get(uri)
55
+ end
56
+ end
57
+
58
+ def test_partial_hijack
59
+ run_app(lambda do |env|
60
+ [200, { "Content-Type" => "text/plain", "rack.hijack" => lambda { |stream|
61
+ stream.write("Hello")
62
+ stream.write(", World!")
63
+ stream.close
64
+ } }, []]
65
+ end) do |uri|
66
+ assert_equal "Hello, World!", Net::HTTP.get(uri)
67
+ end
68
+ end
69
+
70
+ def test_enumerable_body
71
+ run_app(lambda do |env|
72
+ [200, { "Content-Type" => "application/json" },
73
+ %W[one\n two\n three\n]]
74
+ end) do |uri|
75
+ assert_equal "one\ntwo\nthree\n", Net::HTTP.get(uri)
76
+ end
77
+ end
78
+
79
+ require 'debug'
80
+ def test_scheduler_non_blocking
81
+ run_app(
82
+ lambda do |env|
83
+ sleep 0.25
84
+ [200, { "Content-Type" => "text/plain" }, "Response: #{env["PATH_INFO"][1..-1]}"]
85
+ end,
86
+ scheduler_class: "Itsi::Scheduler"
87
+ ) do |uri|
88
+ start_time = Time.now
89
+ 20.times.map do
90
+ Thread.new do
91
+ payload = SecureRandom.hex(16)
92
+ local_uri = uri.dup
93
+ local_uri.path = "/#{payload}"
94
+ response = Net::HTTP.start(local_uri.hostname, local_uri.port) do |http|
95
+ http.request(Net::HTTP::Get.new(local_uri))
96
+ end
97
+ assert_equal "Response: #{payload}", response.body
98
+ end
99
+ end.each(&:join)
100
+ assert_in_delta 0.25, Time.now - start_time, 0.5
101
+ end
102
+ end
103
+
104
+ def test_query_params
105
+ run_app(lambda do |env|
106
+ [200, { "Content-Type" => "text/plain" }, [env["QUERY_STRING"]]]
107
+ end) do |uri|
108
+ uri.query = "foo=bar&baz=qux"
109
+ assert_equal "foo=bar&baz=qux", Net::HTTP.get(uri)
110
+ end
111
+ end
112
+
113
+ def test_put_request
114
+ run_app(lambda do |env|
115
+ body = env["rack.input"].read
116
+ [200, { "Content-Type" => "text/plain" }, [body]]
117
+ end) do |uri|
118
+ uri_obj = URI(uri)
119
+ req = Net::HTTP::Put.new(uri_obj)
120
+ req.body = "put data"
121
+ response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
122
+ assert_equal "put data", response.body
123
+ end
124
+ end
125
+
126
+ def test_custom_headers
127
+ run_app(lambda do |env|
128
+ header = env["HTTP_X_CUSTOM"] || ""
129
+ [200, { "Content-Type" => "text/plain" }, [header]]
130
+ end) do |uri|
131
+ uri_obj = URI(uri)
132
+ req = Net::HTTP::Get.new(uri_obj)
133
+ req["X-Custom"] = "custom-value"
134
+ response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
135
+ assert_equal "custom-value", response.body
136
+ end
137
+ end
138
+
139
+ def test_error_response
140
+ response = nil
141
+ capture_subprocess_io do
142
+ run_app(lambda do |env|
143
+ raise "Intentional error for testing"
144
+ end) do |uri|
145
+ response = Net::HTTP.get_response(uri)
146
+ end
147
+ end
148
+ assert_equal "500", response.code
149
+ end
150
+
151
+ def test_redirect
152
+ run_app(lambda do |env|
153
+ [302, { "Location" => "http://example.com" }, []]
154
+ end) do |uri|
155
+ response = Net::HTTP.get_response(uri)
156
+ assert_equal "302", response.code
157
+ assert_equal "http://example.com", response["location"]
158
+ end
159
+ end
160
+
161
+ def test_not_found
162
+ run_app(lambda do |env|
163
+ if env["PATH_INFO"] == "/"
164
+ [200, { "Content-Type" => "text/plain" }, ["Home"]]
165
+ else
166
+ [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
167
+ end
168
+ end) do |uri|
169
+ uri.path = "/nonexistent"
170
+ response = Net::HTTP.get_response(uri)
171
+ assert_equal "404", response.code
172
+ assert_equal "Not Found", response.body
173
+ end
174
+ end
175
+
176
+ def test_head_request
177
+ run_app(lambda do |env|
178
+ [200, { "Content-Type" => "text/plain", "Content-Length" => "13" }, ["Hello, World!"]]
179
+ end) do |uri|
180
+ uri_obj = URI(uri)
181
+ response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) do |http|
182
+ http.head("/")
183
+ end
184
+ assert_equal "200", response.code
185
+ assert_empty response.body.to_s
186
+ assert_equal "13", response["content-length"]
187
+ end
188
+ end
189
+
190
+ def test_options_request
191
+ run_app(lambda do |env|
192
+ [200, { "Allow" => "GET,POST,OPTIONS", "Content-Type" => "text/plain" }, ["Options Response"]]
193
+ end) do |uri|
194
+ uri_obj = URI(uri)
195
+ req = Net::HTTP::Options.new(uri_obj)
196
+ response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
197
+ assert_equal "200", response.code
198
+ assert_equal "GET,POST,OPTIONS", response["allow"]
199
+ assert_equal "Options Response", response.body
200
+ end
201
+ end
202
+
203
+ def test_cookie_handling
204
+ run_app(lambda do |env|
205
+ [200, { "Content-Type" => "text/plain", "Set-Cookie" => "session=abc123; Path=/" }, ["Cookie Test"]]
206
+ end) do |uri|
207
+ response = Net::HTTP.get_response(uri)
208
+ assert_equal "200", response.code
209
+ assert_match(/session=abc123/, response["set-cookie"])
210
+ assert_equal "Cookie Test", response.body
211
+ end
212
+ end
213
+
214
+ def test_multiple_headers
215
+ run_app(lambda do |env|
216
+ [200, { "Content-Type" => "text/plain", "X-Example" => "one, two, three" }, ["Multiple Headers"]]
217
+ end) do |uri|
218
+ response = Net::HTTP.get_response(uri)
219
+ assert_equal "200", response.code
220
+ assert_equal "one, two, three", response["x-example"]
221
+ assert_equal "Multiple Headers", response.body
222
+ end
223
+ end
224
+
225
+ def test_large_body
226
+ large_text = "A" * 10_000
227
+ run_app(lambda do |env|
228
+ [200, { "Content-Type" => "text/plain", "Content-Length" => large_text.bytesize.to_s }, [large_text]]
229
+ end) do |uri|
230
+ response = Net::HTTP.get_response(uri)
231
+ assert_equal "200", response.code
232
+ assert_equal large_text, response.body
233
+ end
234
+ end
235
+
236
+ def test_custom_status_code
237
+ run_app(lambda do |env|
238
+ [201, { "Content-Type" => "text/plain" }, ["Created"]]
239
+ end) do |uri|
240
+ response = Net::HTTP.get_response(uri)
241
+ assert_equal "201", response.code
242
+ assert_equal "Created", response.body
243
+ end
244
+ end
245
+
246
+ def test_empty_body
247
+ run_app(lambda do |env|
248
+ [204, { "Content-Type" => "text/plain" }, []]
249
+ end) do |uri|
250
+ response = Net::HTTP.get_response(uri)
251
+ assert_equal "204", response.code
252
+ assert_nil response.body
253
+ end
254
+ end
255
+
256
+ def test_utf8_response
257
+ utf8_text = "こんにちは世界"
258
+ run_app(lambda do |env|
259
+ [200, { "Content-Type" => "text/plain; charset=utf-8" }, [utf8_text]]
260
+ end) do |uri|
261
+ response = Net::HTTP.get_response(uri)
262
+ assert_equal "200", response.code
263
+ assert_equal utf8_text, response.body.force_encoding("UTF-8")
264
+ end
265
+ end
266
+
267
+ def test_custom_request_header
268
+ run_app(lambda do |env|
269
+ header_value = env["HTTP_X_MY_HEADER"] || ""
270
+ [200, { "Content-Type" => "text/plain" }, [header_value]]
271
+ end) do |uri|
272
+ uri_obj = URI(uri)
273
+ req = Net::HTTP::Get.new(uri_obj)
274
+ req["X-My-Header"] = "test-header"
275
+ response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
276
+ assert_equal "test-header", response.body
277
+ end
278
+ end
279
+
280
+ def test_url_encoded_query_params
281
+ run_app(lambda do |env|
282
+ [200, { "Content-Type" => "text/plain" }, [env["QUERY_STRING"]]]
283
+ end) do |uri|
284
+ uri.query = "param=%C3%A9" # %C3%A9 represents 'é'
285
+ assert_equal "param=%C3%A9", Net::HTTP.get(uri)
286
+ end
287
+ end
288
+
289
+ def test_https
290
+ run_app(lambda do |env|
291
+ [200, { "Content-Type" => "text/plain" }, ["Hello, HTTPS!"]]
292
+ end, protocol: "https") do |uri|
293
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
294
+ http.request(Net::HTTP::Get.new(uri))
295
+ end
296
+ assert_equal "200", response.code
297
+ assert_equal "Hello, HTTPS!", response.body
298
+ end
299
+ end
9
300
  end
data/lib/itsi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Itsi
2
- VERSION = "0.1.8"
2
+ VERSION = '0.1.11'
3
3
  end