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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/TODO.md +0 -39
- data/cmd/serve.rb +0 -2
- data/cmd/test.rb +76 -20
- data/examples/blog/app/posts/[id]/index.rb +4 -1
- data/examples/blog/app/posts/index.rb +3 -1
- data/examples/mcp-oauth/test/test_app.rb +2 -20
- data/examples/mcp-oauth/test/test_oauth.rb +93 -217
- data/lib/syntropy/http/server_connection.rb +15 -9
- data/lib/syntropy/module_loader.rb +5 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/session.rb +113 -0
- data/lib/syntropy/request.rb +9 -0
- data/lib/syntropy/test.rb +72 -1
- data/lib/syntropy/version.rb +1 -1
- data/syntropy.gemspec +1 -0
- data/test/test_http_server_connection.rb +8 -5
- data/test/test_request_session.rb +254 -0
- metadata +17 -2
- data/examples/mcp-oauth/test/helper.rb +0 -9
data/lib/syntropy/request.rb
CHANGED
|
@@ -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)
|
data/lib/syntropy/version.rb
CHANGED
data/syntropy.gemspec
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
628
|
-
req.set_cookie('
|
|
629
|
-
req.set_cookie('
|
|
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.
|
|
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
|