syntropy 0.30.0 → 0.31.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/bin/syntropy +8 -86
  4. data/cmd/_banner.rb +16 -0
  5. data/cmd/help.rb +12 -0
  6. data/cmd/serve.rb +95 -0
  7. data/cmd/test.rb +40 -0
  8. data/examples/{counter.rb → basic/counter.rb} +1 -1
  9. data/examples/{templates.rb → basic/templates.rb} +1 -1
  10. data/examples/mcp-oauth/.ruby-version +1 -0
  11. data/examples/mcp-oauth/Gemfile +8 -0
  12. data/examples/mcp-oauth/README.md +128 -0
  13. data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
  14. data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
  15. data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
  16. data/examples/mcp-oauth/app/index.md +1 -0
  17. data/examples/mcp-oauth/app/mcp.rb +38 -0
  18. data/examples/mcp-oauth/app/oauth/authorize.rb +26 -0
  19. data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
  20. data/examples/mcp-oauth/app/oauth/register.rb +15 -0
  21. data/examples/mcp-oauth/app/oauth/token.rb +79 -0
  22. data/examples/mcp-oauth/app/signin.rb +85 -0
  23. data/examples/mcp-oauth/test/helper.rb +9 -0
  24. data/examples/mcp-oauth/test/test_app.rb +27 -0
  25. data/examples/mcp-oauth/test/test_oauth.rb +628 -0
  26. data/lib/syntropy/app.rb +15 -4
  27. data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
  28. data/lib/syntropy/applets/builtin/req.rb +1 -1
  29. data/lib/syntropy/dev_mode.rb +1 -1
  30. data/lib/syntropy/errors.rb +6 -0
  31. data/lib/syntropy/http/client.rb +43 -0
  32. data/lib/syntropy/http/client_connection.rb +36 -0
  33. data/lib/syntropy/http/io_extensions.rb +148 -0
  34. data/lib/syntropy/http/server.rb +5 -5
  35. data/lib/syntropy/http/{connection.rb → server_connection.rb} +9 -38
  36. data/lib/syntropy/http.rb +3 -1
  37. data/lib/syntropy/logger.rb +5 -1
  38. data/lib/syntropy/papercraft_extensions.rb +1 -1
  39. data/lib/syntropy/request/mock_adapter.rb +2 -0
  40. data/lib/syntropy/request/request_info.rb +20 -1
  41. data/lib/syntropy/request/response.rb +2 -2
  42. data/lib/syntropy/request/validation.rb +10 -3
  43. data/lib/syntropy/routing_tree.rb +2 -1
  44. data/lib/syntropy/test.rb +65 -0
  45. data/lib/syntropy/version.rb +1 -1
  46. data/lib/syntropy.rb +1 -21
  47. data/syntropy.gemspec +1 -2
  48. data/test/app/.well-known/foo.rb +3 -0
  49. data/test/helper.rb +1 -25
  50. data/test/test_app.rb +53 -68
  51. data/test/test_caching.rb +1 -1
  52. data/test/test_http_client.rb +52 -0
  53. data/test/test_http_client_connection.rb +43 -0
  54. data/test/{test_connection.rb → test_http_server_connection.rb} +29 -29
  55. data/test/test_json_api.rb +4 -4
  56. data/test/{test_request_extensions.rb → test_request.rb} +150 -18
  57. data/test/test_routing_tree.rb +15 -3
  58. metadata +41 -29
  59. data/test/test_request_info.rb +0 -90
  60. /data/examples/{bad.rb → basic/bad.rb} +0 -0
  61. /data/examples/{card.rb → basic/card.rb} +0 -0
  62. /data/examples/{counter.js → basic/counter.js} +0 -0
  63. /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
  64. /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
  65. /data/examples/{index.md → basic/index.md} +0 -0
@@ -2,31 +2,116 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class MethodValidationTest < Minitest::Test
6
- VE = Syntropy::ValidationError
5
+ class RequestInfoTest < Minitest::Test
6
+ def test_uri
7
+ r = Syntropy::MockAdapter.mock(':path' => '/test/path')
8
+ assert_equal '/test/path', r.path
9
+ assert_equal({}, r.query)
7
10
 
8
- def test_validate_http_method
9
- @req = mock_req(':method' => 'GET', ':path' => '/foo')
11
+ r = Syntropy::MockAdapter.mock(':path' => '/test/path?a=1&b=2&c=3%2f4')
12
+ assert_equal '/test/path', r.path
13
+ assert_equal({ a: '1', b: '2', c: '3/4' }, r.query)
14
+ end
10
15
 
11
- assert_equal 'get', @req.validate_http_method('get', 'head')
12
- assert_raises(Syntropy::Error) { @req.validate_http_method('post', 'put') }
13
- assert_raises(Syntropy::Error) { @req.validate_http_method() }
16
+ def test_query
17
+ r = Syntropy::MockAdapter.mock(':path' => '/GponForm/diag_Form?images/')
18
+ assert_equal '/GponForm/diag_Form', r.path
19
+ assert_equal({:'images/' => true}, r.query)
20
+
21
+ r = Syntropy::MockAdapter.mock(':path' => '/?a=1&b=2')
22
+ assert_equal '/', r.path
23
+ assert_equal({a: '1', b: '2'}, r.query)
24
+
25
+ r = Syntropy::MockAdapter.mock(':path' => '/?l=a&t=&x=42')
26
+ assert_equal({l: 'a', t: '', x: '42'}, r.query)
14
27
  end
15
28
 
16
- def test_respond_by_http_method
17
- @req = mock_req(':method' => 'GET', ':path' => '/foo')
29
+ def test_host
30
+ r = Syntropy::MockAdapter.mock(':path' => '/')
31
+ assert_nil r.host
32
+ assert_nil r.authority
18
33
 
19
- @req.respond_by_http_method(
20
- 'get' => ['GET foo', {}],
21
- 'post' => ['POST foo', {}]
34
+ r = Syntropy::MockAdapter.mock('host' => 'my.example.com')
35
+ assert_equal 'my.example.com', r.host
36
+ assert_equal 'my.example.com', r.authority
37
+
38
+ r = Syntropy::MockAdapter.mock(':authority' => 'my.foo.com')
39
+ assert_equal 'my.foo.com', r.host
40
+ assert_equal 'my.foo.com', r.authority
41
+ end
42
+
43
+ def test_full_uri
44
+ r = Syntropy::MockAdapter.mock(
45
+ ':scheme' => 'https',
46
+ 'host' => 'foo.bar',
47
+ ':path' => '/hey?a=b&c=d'
22
48
  )
23
- assert_equal 'GET foo', @req.response_body
24
49
 
25
- assert_raises(Syntropy::Error) {
26
- @req.respond_by_http_method(
27
- 'post' => ['POST foo', {}]
28
- )
29
- }
50
+ assert_equal 'https://foo.bar/hey?a=b&c=d', r.full_uri
51
+ end
52
+
53
+ def test_cookies
54
+ r = Syntropy::MockAdapter.mock
55
+
56
+ assert_equal({}, r.cookies)
57
+
58
+ r = Syntropy::MockAdapter.mock(
59
+ 'cookie' => 'uaid=a%2Fb; lastLocus=settings; signin_ref=/'
60
+ )
61
+
62
+ assert_equal({
63
+ 'uaid' => 'a/b',
64
+ 'lastLocus' => 'settings',
65
+ 'signin_ref' => '/'
66
+ }, r.cookies)
67
+ end
68
+
69
+ def test_content_type
70
+ r = Syntropy::MockAdapter.mock(
71
+ ':scheme' => 'https',
72
+ 'host' => 'foo.bar',
73
+ ':path' => '/hey?a=b&c=d',
74
+ 'content-type' => 'text/plain'
75
+ )
76
+ assert_equal 'text/plain', r.content_type
77
+
78
+ r = Syntropy::MockAdapter.mock(
79
+ ':scheme' => 'https',
80
+ 'host' => 'foo.bar',
81
+ ':path' => '/hey?a=b&c=d',
82
+ 'content-type' => 'text/plain; charset=utf-8'
83
+ )
84
+ assert_equal 'text/plain', r.content_type
85
+
86
+ r = Syntropy::MockAdapter.mock(
87
+ ':scheme' => 'https',
88
+ 'host' => 'foo.bar',
89
+ ':path' => '/hey?a=b&c=d',
90
+ 'content-type' => 'text/plain ; charset=utf-8'
91
+ )
92
+ assert_equal 'text/plain', r.content_type
93
+ end
94
+
95
+ def test_rewrite!
96
+ r = Syntropy::MockAdapter.mock(
97
+ ':scheme' => 'https',
98
+ 'host' => 'foo.bar',
99
+ ':path' => '/hey/ho?a=b&c=d'
100
+ )
101
+
102
+ assert_equal '/hey/ho', r.path
103
+ assert_equal URI.parse('/hey/ho?a=b&c=d'), r.uri
104
+ assert_equal 'https://foo.bar/hey/ho?a=b&c=d', r.full_uri
105
+
106
+ r.rewrite!('/hhh', '/')
107
+ assert_equal '/hey/ho', r.path
108
+ assert_equal URI.parse('/hey/ho?a=b&c=d'), r.uri
109
+ assert_equal 'https://foo.bar/hey/ho?a=b&c=d', r.full_uri
110
+
111
+ r.rewrite!('/hey', '/')
112
+ assert_equal '/ho', r.path
113
+ assert_equal URI.parse('/ho?a=b&c=d'), r.uri
114
+ assert_equal 'https://foo.bar/ho?a=b&c=d', r.full_uri
30
115
  end
31
116
  end
32
117
 
@@ -90,6 +175,53 @@ class ValidationTest < Minitest::Test
90
175
  assert_raises(VE) { @req.validate('foo', :bool) }
91
176
  assert_raises(VE) { @req.validate(nil, :bool) }
92
177
  end
178
+
179
+ def test_validate_http_method
180
+ @req = mock_req(':method' => 'GET', ':path' => '/foo')
181
+
182
+ assert_equal 'get', @req.validate_http_method('get', 'head')
183
+ assert_raises(Syntropy::Error) { @req.validate_http_method('post', 'put') }
184
+ assert_raises(Syntropy::Error) { @req.validate_http_method() }
185
+ end
186
+
187
+ def test_respond_by_http_method
188
+ @req = mock_req(':method' => 'GET', ':path' => '/foo')
189
+
190
+ @req.respond_by_http_method(
191
+ 'get' => ['GET foo', {}],
192
+ 'post' => ['POST foo', {}]
193
+ )
194
+ assert_equal 'GET foo', @req.response_body
195
+
196
+ assert_raises(Syntropy::Error) {
197
+ @req.respond_by_http_method(
198
+ 'post' => ['POST foo', {}]
199
+ )
200
+ }
201
+ end
202
+
203
+ def test_validate_content_type
204
+ @req = mock_req(':method' => 'POST', ':path' => '/foo')
205
+
206
+ assert_raises(Syntropy::InvalidRequestContentTypeError) {
207
+ @req.validate_content_type('application/json')
208
+ }
209
+
210
+ @req = mock_req(':method' => 'POST', ':path' => '/foo', 'content-type' => 'application/json')
211
+ assert_equal 'application/json', @req.validate_content_type(
212
+ 'application/json', 'text/plain'
213
+ )
214
+
215
+ @req = mock_req(':method' => 'POST', ':path' => '/foo', 'content-type' => 'text/html; charset=utf-8')
216
+ assert_equal 'text/html', @req.validate_content_type(
217
+ 'text/plain', 'text/html'
218
+ )
219
+
220
+ @req = mock_req(':method' => 'POST', ':path' => '/foo', 'content-type' => 'text/html ; charset=utf-8')
221
+ assert_equal 'text/html', @req.validate_content_type(
222
+ 'text/plain', 'text/html'
223
+ )
224
+ end
93
225
  end
94
226
 
95
227
  class FormDataTest < Minitest::Test
@@ -46,6 +46,9 @@ class RoutingTreeTest < Minitest::Test
46
46
  'old': {
47
47
  'index.html': '',
48
48
  'baz.html': ''
49
+ },
50
+ '.well-known': {
51
+ 'foo.rb': ''
49
52
  }
50
53
  }
51
54
  }
@@ -72,7 +75,9 @@ class RoutingTreeTest < Minitest::Test
72
75
  assert_nil root[:parent]
73
76
  assert_nil root[:param]
74
77
  assert_nil root[:target]
75
- assert_equal ['[]', 'about', 'api', 'assets', 'old', 'posts'], root[:children].keys.sort_by(&:to_s)
78
+ assert_equal [
79
+ '.well-known', '[]', 'about', 'api', 'assets', 'old', 'posts'
80
+ ], root[:children].keys.sort_by(&:to_s)
76
81
 
77
82
  entry = @rt.static_map['/docs']
78
83
  assert_equal File.join(@rt.root_dir, 'index.rb'), entry[:target][:fn]
@@ -132,18 +137,25 @@ class RoutingTreeTest < Minitest::Test
132
137
  assets = root[:children]['assets']
133
138
  refute_nil assets
134
139
  assert_nil assets[:target]
140
+
141
+ well_known = root[:children]['.well-known']
142
+ refute_nil well_known
143
+ assert_nil well_known[:target]
135
144
  end
136
145
 
137
146
  def test_static_map
138
147
  map = @rt.static_map
139
- assert_equal 7, map.size
148
+ assert_equal 8, map.size
140
149
 
141
150
  keys = map.keys.sort
142
151
  assert_equal [
143
- '/docs', '/docs/about', '/docs/assets/css/style.css',
152
+ '/docs', '/docs/.well-known/foo', '/docs/about', '/docs/assets/css/style.css',
144
153
  '/docs/assets/img/foo.jpg', '/docs/old', '/docs/old/baz', '/docs/posts'
145
154
  ], keys
146
155
 
156
+ o = map['/docs/.well-known/foo']
157
+ assert_equal File.join(@rt.root_dir, '.well-known/foo.rb'), o[:target][:fn]
158
+
147
159
  o = map['/docs/assets/css/style.css']
148
160
  assert_equal File.join(@rt.root_dir, 'assets/css/style.css'), o[:target][:fn]
149
161
 
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.30.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -43,28 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 1.0.0
46
+ version: 1.0.2
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 1.0.0
54
- - !ruby/object:Gem::Dependency
55
- name: cgi
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
53
+ version: 1.0.2
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: escape_utils
70
56
  requirement: !ruby/object:Gem::Requirement
@@ -153,6 +139,9 @@ files:
153
139
  - Rakefile
154
140
  - TODO.md
155
141
  - bin/syntropy
142
+ - cmd/_banner.rb
143
+ - cmd/help.rb
144
+ - cmd/serve.rb
156
145
  - cmd/setup/template/site/.gitignore
157
146
  - cmd/setup/template/site/Dockerfile
158
147
  - cmd/setup/template/site/Gemfile
@@ -176,15 +165,32 @@ files:
176
165
  - cmd/setup/template/site/site/assets/css/style.css
177
166
  - cmd/setup/template/site/site/assets/img/syntropy.png
178
167
  - cmd/setup/template/site/site/index.rb
168
+ - cmd/test.rb
179
169
  - docker-compose.yml
180
- - examples/bad.rb
181
- - examples/card.rb
182
- - examples/counter.js
183
- - examples/counter.rb
184
- - examples/counter_api.rb
185
- - examples/favicon.ico
186
- - examples/index.md
187
- - examples/templates.rb
170
+ - examples/basic/bad.rb
171
+ - examples/basic/card.rb
172
+ - examples/basic/counter.js
173
+ - examples/basic/counter.rb
174
+ - examples/basic/counter_api.rb
175
+ - examples/basic/favicon.ico
176
+ - examples/basic/index.md
177
+ - examples/basic/templates.rb
178
+ - examples/mcp-oauth/.ruby-version
179
+ - examples/mcp-oauth/Gemfile
180
+ - examples/mcp-oauth/README.md
181
+ - examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb
182
+ - examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb
183
+ - examples/mcp-oauth/app/_lib/auth_store.rb
184
+ - examples/mcp-oauth/app/index.md
185
+ - examples/mcp-oauth/app/mcp.rb
186
+ - examples/mcp-oauth/app/oauth/authorize.rb
187
+ - examples/mcp-oauth/app/oauth/consent.rb
188
+ - examples/mcp-oauth/app/oauth/register.rb
189
+ - examples/mcp-oauth/app/oauth/token.rb
190
+ - examples/mcp-oauth/app/signin.rb
191
+ - examples/mcp-oauth/test/helper.rb
192
+ - examples/mcp-oauth/test/test_app.rb
193
+ - examples/mcp-oauth/test/test_oauth.rb
188
194
  - lib/syntropy.rb
189
195
  - lib/syntropy/app.rb
190
196
  - lib/syntropy/applets/builtin/auto_refresh/watch.js
@@ -200,8 +206,11 @@ files:
200
206
  - lib/syntropy/dev_mode.rb
201
207
  - lib/syntropy/errors.rb
202
208
  - lib/syntropy/http.rb
203
- - lib/syntropy/http/connection.rb
209
+ - lib/syntropy/http/client.rb
210
+ - lib/syntropy/http/client_connection.rb
211
+ - lib/syntropy/http/io_extensions.rb
204
212
  - lib/syntropy/http/server.rb
213
+ - lib/syntropy/http/server_connection.rb
205
214
  - lib/syntropy/http/status.rb
206
215
  - lib/syntropy/json_api.rb
207
216
  - lib/syntropy/logger.rb
@@ -216,9 +225,11 @@ files:
216
225
  - lib/syntropy/request/validation.rb
217
226
  - lib/syntropy/routing_tree.rb
218
227
  - lib/syntropy/side_run.rb
228
+ - lib/syntropy/test.rb
219
229
  - lib/syntropy/utils.rb
220
230
  - lib/syntropy/version.rb
221
231
  - syntropy.gemspec
232
+ - test/app/.well-known/foo.rb
222
233
  - test/app/_hook.rb
223
234
  - test/app/_layout/default.rb
224
235
  - test/app/_lib/callable.rb
@@ -255,14 +266,15 @@ files:
255
266
  - test/run.rb
256
267
  - test/test_app.rb
257
268
  - test/test_caching.rb
258
- - test/test_connection.rb
259
269
  - test/test_connection_pool.rb
260
270
  - test/test_errors.rb
271
+ - test/test_http_client.rb
272
+ - test/test_http_client_connection.rb
273
+ - test/test_http_server_connection.rb
261
274
  - test/test_json_api.rb
262
275
  - test/test_mock_adapter.rb
263
276
  - test/test_module.rb
264
- - test/test_request_extensions.rb
265
- - test/test_request_info.rb
277
+ - test/test_request.rb
266
278
  - test/test_response.rb
267
279
  - test/test_routing_tree.rb
268
280
  - test/test_server.rb
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'helper'
4
-
5
- class RequestInfoTest < Minitest::Test
6
- def test_uri
7
- r = Syntropy::MockAdapter.mock(':path' => '/test/path')
8
- assert_equal '/test/path', r.path
9
- assert_equal({}, r.query)
10
-
11
- r = Syntropy::MockAdapter.mock(':path' => '/test/path?a=1&b=2&c=3%2f4')
12
- assert_equal '/test/path', r.path
13
- assert_equal({ a: '1', b: '2', c: '3/4' }, r.query)
14
- end
15
-
16
- def test_query
17
- r = Syntropy::MockAdapter.mock(':path' => '/GponForm/diag_Form?images/')
18
- assert_equal '/GponForm/diag_Form', r.path
19
- assert_equal({:'images/' => true}, r.query)
20
-
21
- r = Syntropy::MockAdapter.mock(':path' => '/?a=1&b=2')
22
- assert_equal '/', r.path
23
- assert_equal({a: '1', b: '2'}, r.query)
24
-
25
- r = Syntropy::MockAdapter.mock(':path' => '/?l=a&t=&x=42')
26
- assert_equal({l: 'a', t: '', x: '42'}, r.query)
27
- end
28
-
29
- def test_host
30
- r = Syntropy::MockAdapter.mock(':path' => '/')
31
- assert_nil r.host
32
- assert_nil r.authority
33
-
34
- r = Syntropy::MockAdapter.mock('host' => 'my.example.com')
35
- assert_equal 'my.example.com', r.host
36
- assert_equal 'my.example.com', r.authority
37
-
38
- r = Syntropy::MockAdapter.mock(':authority' => 'my.foo.com')
39
- assert_equal 'my.foo.com', r.host
40
- assert_equal 'my.foo.com', r.authority
41
- end
42
-
43
- def test_full_uri
44
- r = Syntropy::MockAdapter.mock(
45
- ':scheme' => 'https',
46
- 'host' => 'foo.bar',
47
- ':path' => '/hey?a=b&c=d'
48
- )
49
-
50
- assert_equal 'https://foo.bar/hey?a=b&c=d', r.full_uri
51
- end
52
-
53
- def test_cookies
54
- r = Syntropy::MockAdapter.mock
55
-
56
- assert_equal({}, r.cookies)
57
-
58
- r = Syntropy::MockAdapter.mock(
59
- 'cookie' => 'uaid=a%2Fb; lastLocus=settings; signin_ref=/'
60
- )
61
-
62
- assert_equal({
63
- 'uaid' => 'a/b',
64
- 'lastLocus' => 'settings',
65
- 'signin_ref' => '/'
66
- }, r.cookies)
67
- end
68
-
69
- def test_rewrite!
70
- r = Syntropy::MockAdapter.mock(
71
- ':scheme' => 'https',
72
- 'host' => 'foo.bar',
73
- ':path' => '/hey/ho?a=b&c=d'
74
- )
75
-
76
- assert_equal '/hey/ho', r.path
77
- assert_equal URI.parse('/hey/ho?a=b&c=d'), r.uri
78
- assert_equal 'https://foo.bar/hey/ho?a=b&c=d', r.full_uri
79
-
80
- r.rewrite!('/hhh', '/')
81
- assert_equal '/hey/ho', r.path
82
- assert_equal URI.parse('/hey/ho?a=b&c=d'), r.uri
83
- assert_equal 'https://foo.bar/hey/ho?a=b&c=d', r.full_uri
84
-
85
- r.rewrite!('/hey', '/')
86
- assert_equal '/ho', r.path
87
- assert_equal URI.parse('/ho?a=b&c=d'), r.uri
88
- assert_equal 'https://foo.bar/ho?a=b&c=d', r.full_uri
89
- end
90
- end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes