syntropy 0.32.0 → 0.33.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.
@@ -3,6 +3,7 @@
3
3
  require_relative './request/request_info'
4
4
  require_relative './request/validation'
5
5
  require_relative './request/response'
6
+ require_relative './request/session'
6
7
  require_relative './http/status'
7
8
 
8
9
  module Syntropy
@@ -95,5 +96,13 @@ module Syntropy
95
96
  def total_transfer
96
97
  (headers[':rx'] || 0) + (headers[':tx'] || 0)
97
98
  end
99
+
100
+ def session
101
+ @session ||= Session.new(self)
102
+ end
103
+
104
+ def flash
105
+ session.flash
106
+ end
98
107
  end
99
108
  end
data/lib/syntropy/test.rb CHANGED
@@ -3,8 +3,80 @@
3
3
  require 'syntropy'
4
4
  require 'syntropy/request/mock_adapter'
5
5
  require 'minitest'
6
+ require 'json'
7
+ require 'uri'
6
8
 
7
9
  module Syntropy
10
+ class Test < Minitest::Test
11
+ HTTP = Syntropy::HTTP
12
+
13
+ def self.env=(env)
14
+ @@env = env
15
+ end
16
+
17
+ attr_reader :machine, :app
18
+
19
+ def env
20
+ @@env
21
+ end
22
+
23
+ def load_module(ref)
24
+ app.module_loader.load(ref)
25
+ end
26
+
27
+ def http_request(headers, body = nil)
28
+ @test_harness.request(headers, body)
29
+ end
30
+
31
+ def get(path, **headers)
32
+ http_request(
33
+ headers.merge(
34
+ ':method' => 'GET',
35
+ ':path' => path
36
+ )
37
+ )
38
+ end
39
+
40
+ def post(path, content_type, body, **headers)
41
+ headers = headers.merge('content-type' => content_type) if content_type
42
+ http_request(
43
+ headers.merge(
44
+ {
45
+ ':method' => 'POST',
46
+ ':path' => path
47
+ }
48
+ ),
49
+ body
50
+ )
51
+ end
52
+
53
+ def post_json(path, obj, **)
54
+ post(path, 'application/json', JSON.dump(obj), **)
55
+ end
56
+
57
+ def post_form(path, form, **)
58
+ post(path, 'application/x-www-form-urlencoded', URI.encode_www_form(form), **)
59
+ end
60
+
61
+ def setup
62
+ raise 'Environment not set' if !@@env
63
+
64
+ @machine = UM.new
65
+ @app = Syntropy::App.new(
66
+ root_dir: @@env[:root_dir],
67
+ mount_path: '/',
68
+ machine: @machine
69
+ )
70
+ @test_harness = Syntropy::TestHarness.new(@app)
71
+ end
72
+
73
+ def teardown
74
+ @machine = nil
75
+ @app = nil
76
+ @test_harness = nil
77
+ end
78
+ end
79
+
8
80
  class TestHarness
9
81
  def initialize(app)
10
82
  @app = app
@@ -28,7 +100,6 @@ module Syntropy
28
100
  end
29
101
  end
30
102
 
31
-
32
103
  private
33
104
 
34
105
  def mock_req(headers, body = nil)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.32.0'
4
+ VERSION = '0.33.0'
5
5
  end
data/syntropy.gemspec CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency 'uringmachine', '~>1.0.2'
27
27
 
28
28
  s.add_dependency 'json'
29
+ s.add_dependency 'base64'
29
30
  s.add_dependency 'logger'
30
31
  s.add_dependency 'irb'
31
32
 
@@ -596,7 +596,7 @@ class HTTPServerConnectionTest < Minitest::Test
596
596
 
597
597
  def test_set_cookie_single
598
598
  @hook = ->(req) {
599
- req.set_cookie('foo=bar; HttpOnly')
599
+ req.set_cookie('foo', 'bar; HttpOnly')
600
600
  req.respond('foo')
601
601
  }
602
602
 
@@ -610,7 +610,8 @@ class HTTPServerConnectionTest < Minitest::Test
610
610
 
611
611
  def test_set_cookie_multi1
612
612
  @hook = ->(req) {
613
- req.set_cookie('foo=bar; HttpOnly', 'bar=baz')
613
+ req.set_cookie('foo', 'bar; HttpOnly')
614
+ req.set_cookie('bar', 'baz')
614
615
  req.respond('foo')
615
616
  }
616
617
 
@@ -624,9 +625,11 @@ class HTTPServerConnectionTest < Minitest::Test
624
625
 
625
626
  def test_set_cookie_multi2
626
627
  @hook = ->(req) {
627
- req.set_cookie('a=1', 'b=2')
628
- req.set_cookie('c=3')
629
- req.set_cookie('d=4', 'e=5')
628
+ req.set_cookie('a', '1')
629
+ req.set_cookie('b', '2')
630
+ req.set_cookie('c', '3')
631
+ req.set_cookie('d', '4')
632
+ req.set_cookie('e', '5')
630
633
  req.respond('foo')
631
634
  }
632
635
 
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class RequestSessionTest < Minitest::Test
6
+ def make_socket_pair
7
+ port = SecureRandom.random_number(10000..40000)
8
+ server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
9
+ @machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
10
+ @machine.bind(server_fd, '127.0.0.1', port)
11
+ @machine.listen(server_fd, UM::SOMAXCONN)
12
+
13
+ client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
14
+ @machine.connect(client_conn_fd, '127.0.0.1', port)
15
+
16
+ server_conn_fd = @machine.accept(server_fd)
17
+
18
+ @machine.close(server_fd)
19
+ [client_conn_fd, server_conn_fd]
20
+ end
21
+
22
+ def setup
23
+ @machine = UM.new
24
+ @c_fd, @s_fd = make_socket_pair
25
+ # s = @machine.io(@s_fd, :socket)
26
+
27
+ @app = ->(req) { req.respond(nil, ':status' => Syntropy::HTTP::INTERNAL_SERVER_ERROR) }
28
+ @env = {}
29
+ @connection = Syntropy::HTTP::ServerConnection.new(@machine, @s_fd, @env) { |req| @app.(req) }
30
+ end
31
+
32
+ def teardown
33
+ @machine.close(@c_fd) rescue nil
34
+ @machine.close(@s_fd) rescue nil
35
+ end
36
+
37
+ def write_http_request(msg, shutdown_wr = true)
38
+ @machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
39
+ @machine.shutdown(@c_fd, UM::SHUT_WR) if shutdown_wr
40
+ end
41
+
42
+ def write_client_side(msg)
43
+ @machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
44
+ end
45
+
46
+ def read_client_side(len = 65536)
47
+ buf = +''
48
+ res = @machine.recv(@c_fd, buf, len, 0)
49
+ res == 0 ? nil : buf
50
+ end
51
+
52
+ def test_session_kv_access
53
+ current = :something
54
+
55
+ @app = ->(req) {
56
+ req.session['foo'] = 'bar'
57
+ current = req.session['foo']
58
+ req.respond(nil)
59
+ }
60
+
61
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
62
+ @connection.serve_request
63
+
64
+ assert_equal 'bar', current
65
+
66
+ response = read_client_side
67
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
68
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
69
+ end
70
+
71
+ def test_session_kv_multi
72
+ @app = ->(req) {
73
+ req.session['foo'] = 'bar'
74
+ req.session['bar'] = 'baz'
75
+ req.respond(nil)
76
+ }
77
+
78
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
79
+ @connection.serve_request
80
+
81
+ response = read_client_side
82
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar', 'bar' => 'baz' }))
83
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
84
+ end
85
+
86
+ def test_session_kv_sequence
87
+ counter = 0
88
+
89
+ @app = ->(req) {
90
+ counter += 1
91
+ case counter
92
+ when 1
93
+ req.session['foo'] = 'bar'
94
+ when 2
95
+ req.session['foo'] = req.session['foo'] + 'baz'
96
+ end
97
+ req.respond(nil)
98
+ }
99
+
100
+ write_http_request "GET / HTTP/1.1\r\n\r\n", false
101
+ @connection.serve_request
102
+ response = read_client_side
103
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
104
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
105
+
106
+ write_http_request "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{data}\r\n\r\n"
107
+ @connection.serve_request
108
+ response = read_client_side
109
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'barbaz' }))
110
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
111
+ end
112
+
113
+ def test_session_kv_delete
114
+ counter = 0
115
+
116
+ @app = ->(req) {
117
+ counter += 1
118
+ case counter
119
+ when 1
120
+ req.session['foo'] = 'bar'
121
+ when 2
122
+ req.session.delete('foo')
123
+ end
124
+ req.respond(nil)
125
+ }
126
+
127
+ write_http_request "GET / HTTP/1.1\r\n\r\n", false
128
+ @connection.serve_request
129
+ response = read_client_side
130
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
131
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
132
+
133
+ write_http_request "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{data}\r\n\r\n"
134
+ @connection.serve_request
135
+ response = read_client_side
136
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly\r\n\r\n", response
137
+ end
138
+
139
+ def test_session_discard
140
+ counter = 0
141
+
142
+ @app = ->(req) {
143
+ counter += 1
144
+ case counter
145
+ when 1
146
+ req.session['foo'] = 'bar'
147
+ when 2
148
+ req.session.discard
149
+ end
150
+ req.respond(nil)
151
+ }
152
+
153
+ write_http_request "GET / HTTP/1.1\r\n\r\n", false
154
+ @connection.serve_request
155
+ response = read_client_side
156
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
157
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
158
+
159
+ write_http_request "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{data}\r\n\r\n"
160
+ @connection.serve_request
161
+ response = read_client_side
162
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly\r\n\r\n", response
163
+ end
164
+
165
+ def test_flash_simple
166
+ counter = 0
167
+ flash_notices = []
168
+
169
+ @app = ->(req) do
170
+ counter += 1
171
+ case counter
172
+ when 1
173
+ req.session.flash[:notice] = "Hello flash!"
174
+ flash_notices << req.session.flash[:notice]
175
+ when 2
176
+ flash_notices << req.session.flash[:notice]
177
+ when 3
178
+ flash_notices << req.session.flash[:notice]
179
+ end
180
+ req.respond(nil)
181
+ end
182
+
183
+ parse_cookie = ->(response) {
184
+ m = response.match(/Set-Cookie: __syntropy_session__=([^\s;]*)/)
185
+ m && m[1]
186
+ }
187
+
188
+ cookie = nil
189
+
190
+ 3.times {
191
+ request = cookie ? "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{cookie}\r\n\r\n" : "GET / HTTP/1.1\r\n\r\n"
192
+ write_http_request request, false
193
+ @connection.serve_request
194
+ response = read_client_side
195
+ v = parse_cookie.(response)
196
+ if v
197
+ cookie = v.empty? ? nil : v
198
+ end
199
+ }
200
+
201
+ assert_equal [nil, 'Hello flash!', nil], flash_notices
202
+ end
203
+
204
+ def test_flash_each
205
+ counter = 0
206
+ flash_content = []
207
+
208
+ @app = ->(req) do
209
+ counter += 1
210
+ case counter
211
+ when 1
212
+ req.session.flash[:notice] = "Hello flash!"
213
+ a = []
214
+ req.session.flash.each { |k, v| a << [k, v] }
215
+ flash_content << a
216
+ when 2
217
+ a = []
218
+ req.session.flash.each { |k, v| a << [k, v] }
219
+ flash_content << a
220
+ when 3
221
+ a = []
222
+ req.session.flash.each { |k, v| a << [k, v] }
223
+ flash_content << a
224
+ end
225
+ req.respond(nil)
226
+ end
227
+
228
+ parse_cookie = ->(response) {
229
+ m = response.match(/Set-Cookie: __syntropy_session__=([^\s;]*)/)
230
+ m && m[1]
231
+ }
232
+
233
+ set_cookies = []
234
+ cookie = nil
235
+
236
+ 3.times {
237
+ request = cookie ? "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{cookie}\r\n\r\n" : "GET / HTTP/1.1\r\n\r\n"
238
+ write_http_request request, false
239
+ @connection.serve_request
240
+ response = read_client_side
241
+ v = parse_cookie.(response)
242
+ if v
243
+ cookie = v.empty? ? nil : v
244
+ end
245
+ set_cookies << v ? cookie : nil
246
+ }
247
+
248
+ assert_equal [
249
+ [],
250
+ [[:notice, 'Hello flash!']],
251
+ []
252
+ ], flash_content
253
+ end
254
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.0
4
+ version: 0.33.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: base64
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: logger
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -212,7 +226,6 @@ files:
212
226
  - examples/mcp-oauth/app/oauth/register.rb
213
227
  - examples/mcp-oauth/app/oauth/token.rb
214
228
  - examples/mcp-oauth/app/signin.rb
215
- - examples/mcp-oauth/test/helper.rb
216
229
  - examples/mcp-oauth/test/test_app.rb
217
230
  - examples/mcp-oauth/test/test_oauth.rb
218
231
  - lib/syntropy.rb
@@ -248,6 +261,7 @@ files:
248
261
  - lib/syntropy/request/mock_adapter.rb
249
262
  - lib/syntropy/request/request_info.rb
250
263
  - lib/syntropy/request/response.rb
264
+ - lib/syntropy/request/session.rb
251
265
  - lib/syntropy/request/validation.rb
252
266
  - lib/syntropy/routing_tree.rb
253
267
  - lib/syntropy/side_run.rb
@@ -311,6 +325,7 @@ files:
311
325
  - test/test_mock_adapter.rb
312
326
  - test/test_module_loader.rb
313
327
  - test/test_request.rb
328
+ - test/test_request_session.rb
314
329
  - test/test_response.rb
315
330
  - test/test_routing_tree.rb
316
331
  - test/test_server.rb
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # require 'bundler/setup'
4
- # require 'syntropy'
5
- # require 'syntropy/test'
6
- # require 'minitest/autorun'
7
-
8
- # STDOUT.sync = true
9
- # STDERR.sync = true