syntropy 0.31.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/.gitignore +2 -0
- data/CHANGELOG.md +20 -0
- data/TODO.md +7 -1
- data/cmd/console.rb +77 -0
- data/cmd/serve.rb +1 -3
- data/cmd/test.rb +76 -20
- data/examples/blog/app/_layout/default.rb +11 -0
- data/examples/blog/app/_lib/post_store.rb +47 -0
- data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/blog/app/_setup.rb +4 -0
- data/examples/blog/app/index.rb +7 -0
- data/examples/blog/app/posts/[id]/edit.rb +33 -0
- data/examples/blog/app/posts/[id]/index.rb +61 -0
- data/examples/blog/app/posts/index.rb +40 -0
- data/examples/blog/app/posts/new.rb +29 -0
- data/examples/mcp-oauth/README.md +3 -3
- data/examples/mcp-oauth/app/mcp.rb +55 -8
- data/examples/mcp-oauth/app/oauth/authorize.rb +0 -8
- data/examples/mcp-oauth/app/oauth/register.rb +0 -1
- data/examples/mcp-oauth/test/test_app.rb +2 -20
- data/examples/mcp-oauth/test/test_oauth.rb +93 -217
- data/lib/syntropy/app.rb +23 -9
- data/lib/syntropy/db/connection_pool.rb +71 -0
- data/lib/syntropy/db/schema.rb +92 -0
- data/lib/syntropy/db/store.rb +31 -0
- data/lib/syntropy/http/io_extensions.rb +33 -5
- data/lib/syntropy/http/server_connection.rb +21 -62
- data/lib/syntropy/{module.rb → module_loader.rb} +48 -8
- data/lib/syntropy/request/request_info.rb +3 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/session.rb +113 -0
- data/lib/syntropy/request/validation.rb +1 -2
- data/lib/syntropy/request.rb +9 -0
- data/lib/syntropy/test.rb +84 -1
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +4 -2
- data/syntropy.gemspec +3 -1
- data/test/app/_hook.rb +1 -1
- data/test/app/by_method.rb +9 -0
- data/test/app_setup/_setup.rb +7 -0
- data/test/app_setup/index.rb +1 -0
- data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
- data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
- data/test/schema/2026-01-02-foo.rb +12 -0
- data/test/schema/2026-05-30-bar.rb +7 -0
- data/test/test_app.rb +58 -3
- data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
- data/test/test_db_schema.rb +96 -0
- data/test/test_db_store.rb +24 -0
- data/test/test_http_protocol.rb +250 -0
- data/test/test_http_server_connection.rb +18 -24
- data/test/test_json_api.rb +1 -1
- data/test/{test_module.rb → test_module_loader.rb} +31 -0
- data/test/test_request.rb +7 -4
- data/test/test_request_session.rb +254 -0
- data/test/test_server.rb +9 -13
- metadata +63 -12
- data/examples/mcp-oauth/test/helper.rb +0 -9
- data/lib/syntropy/connection_pool.rb +0 -61
data/lib/syntropy/test.rb
CHANGED
|
@@ -3,12 +3,84 @@
|
|
|
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
|
|
11
|
-
@app.
|
|
83
|
+
@app.raise_internal_server_error = true if @app.respond_to?(:raise_internal_server_error=)
|
|
12
84
|
end
|
|
13
85
|
|
|
14
86
|
def request(headers, body = nil)
|
|
@@ -17,6 +89,17 @@ module Syntropy
|
|
|
17
89
|
req
|
|
18
90
|
end
|
|
19
91
|
|
|
92
|
+
def no_raise_internal_server_error
|
|
93
|
+
return yield if !@app.respond_to?(:raise_internal_server_error=)
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
@app.raise_internal_server_error = false
|
|
97
|
+
yield
|
|
98
|
+
ensure
|
|
99
|
+
@app.raise_internal_server_error = true
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
20
103
|
private
|
|
21
104
|
|
|
22
105
|
def mock_req(headers, body = nil)
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -8,10 +8,12 @@ require 'syntropy/logger'
|
|
|
8
8
|
require 'syntropy/http'
|
|
9
9
|
require 'syntropy/mime_types'
|
|
10
10
|
require 'syntropy/app'
|
|
11
|
-
require 'syntropy/connection_pool'
|
|
11
|
+
require 'syntropy/db/connection_pool'
|
|
12
|
+
require 'syntropy/db/schema'
|
|
13
|
+
require 'syntropy/db/store'
|
|
12
14
|
require 'syntropy/errors'
|
|
13
15
|
require 'syntropy/markdown'
|
|
14
|
-
require 'syntropy/
|
|
16
|
+
require 'syntropy/module_loader'
|
|
15
17
|
require 'syntropy/papercraft_extensions'
|
|
16
18
|
require 'syntropy/routing_tree'
|
|
17
19
|
require 'syntropy/json_api'
|
data/syntropy.gemspec
CHANGED
|
@@ -24,11 +24,13 @@ Gem::Specification.new do |s|
|
|
|
24
24
|
s.add_dependency 'extralite', '~>2.14'
|
|
25
25
|
s.add_dependency 'papercraft', '~>3.2.0'
|
|
26
26
|
s.add_dependency 'uringmachine', '~>1.0.2'
|
|
27
|
-
s.add_dependency 'escape_utils', '1.3.0'
|
|
28
27
|
|
|
29
28
|
s.add_dependency 'json'
|
|
29
|
+
s.add_dependency 'base64'
|
|
30
30
|
s.add_dependency 'logger'
|
|
31
|
+
s.add_dependency 'irb'
|
|
31
32
|
|
|
32
33
|
s.add_development_dependency 'minitest', '~>6.0.1'
|
|
33
34
|
s.add_development_dependency 'rake', '~>13.3.1'
|
|
35
|
+
s.add_development_dependency 'solargraph'
|
|
34
36
|
end
|
data/test/app/_hook.rb
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export ->(req) { req.respond('foo') }
|
data/test/test_app.rb
CHANGED
|
@@ -114,12 +114,25 @@ class AppTest < Minitest::Test
|
|
|
114
114
|
req = @test_harness.request(':method' => 'GET', ':path' => '/test/rss')
|
|
115
115
|
assert_equal '<link>foo</link>', req.response_body
|
|
116
116
|
|
|
117
|
-
req = @test_harness.
|
|
117
|
+
req = @test_harness.no_raise_internal_server_error {
|
|
118
|
+
@test_harness.request(':method' => 'GET', ':path' => '/test/bad_mod')
|
|
119
|
+
}
|
|
118
120
|
assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
|
|
119
121
|
|
|
120
122
|
req = @test_harness.request(':method' => 'GET', ':path' => '/test/.well-known/foo')
|
|
121
123
|
assert_equal HTTP::OK, req.response_status
|
|
122
124
|
assert_equal 'foo', req.response_body
|
|
125
|
+
|
|
126
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/by_method')
|
|
127
|
+
assert_equal HTTP::OK, req.response_status
|
|
128
|
+
assert_equal 'foo', req.response_body
|
|
129
|
+
|
|
130
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/by_method')
|
|
131
|
+
assert_equal HTTP::OK, req.response_status
|
|
132
|
+
assert_equal 'bar', req.response_body
|
|
133
|
+
|
|
134
|
+
req = @test_harness.request(':method' => 'DELETE', ':path' => '/test/by_method')
|
|
135
|
+
assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
|
|
123
136
|
end
|
|
124
137
|
|
|
125
138
|
def test_automatic_redirect_on_trailing_slash
|
|
@@ -129,14 +142,14 @@ class AppTest < Minitest::Test
|
|
|
129
142
|
end
|
|
130
143
|
|
|
131
144
|
def test_app_file_watching
|
|
132
|
-
@machine.sleep 0.
|
|
145
|
+
@machine.sleep 0.2
|
|
133
146
|
|
|
134
147
|
req = @test_harness.request(':method' => 'GET', ':path' => @tmp_path)
|
|
135
148
|
assert_equal 'foo', req.response_body
|
|
136
149
|
|
|
137
150
|
orig_body = IO.read(@tmp_fn)
|
|
138
151
|
IO.write(@tmp_fn, orig_body.gsub('foo', 'bar'))
|
|
139
|
-
@machine.sleep(0.
|
|
152
|
+
@machine.sleep(0.2)
|
|
140
153
|
|
|
141
154
|
req = @test_harness.request(':method' => 'GET', ':path' => @tmp_path)
|
|
142
155
|
assert_equal 'bar', req.response_body
|
|
@@ -309,3 +322,45 @@ class AppDependenciesTest < Minitest::Test
|
|
|
309
322
|
assert_equal HTTP::OK, req.response_status
|
|
310
323
|
end
|
|
311
324
|
end
|
|
325
|
+
|
|
326
|
+
class AppDBSetupDBTest < Minitest::Test
|
|
327
|
+
HTTP = Syntropy::HTTP
|
|
328
|
+
|
|
329
|
+
APP_ROOT = File.join(__dir__, 'app_with_schema')
|
|
330
|
+
|
|
331
|
+
def test_app_setup_db
|
|
332
|
+
machine = UM.new
|
|
333
|
+
|
|
334
|
+
app = Syntropy::App.new(
|
|
335
|
+
root_dir: APP_ROOT,
|
|
336
|
+
mount_path: '/test',
|
|
337
|
+
machine: machine
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
assert_equal false, app.respond_to?(:connection_pool)
|
|
341
|
+
assert_equal false, app.respond_to?(:schema)
|
|
342
|
+
|
|
343
|
+
fn = "/tmp/#{rand(100000)}.db"
|
|
344
|
+
|
|
345
|
+
app.setup_db(
|
|
346
|
+
db_path: fn,
|
|
347
|
+
schema_root: '_schema'
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
assert_equal true, app.respond_to?(:connection_pool)
|
|
351
|
+
assert_equal fn, app.connection_pool.with_db { it.filename }
|
|
352
|
+
|
|
353
|
+
assert_equal true, app.respond_to?(:schema)
|
|
354
|
+
app.schema.apply(app.connection_pool)
|
|
355
|
+
assert_equal '2026-05-30-bar', app.schema.current_version(app.connection_pool)
|
|
356
|
+
|
|
357
|
+
assert_equal [
|
|
358
|
+
{
|
|
359
|
+
id: 1,
|
|
360
|
+
title: 'foo',
|
|
361
|
+
body: 'baz'
|
|
362
|
+
}
|
|
363
|
+
], app.connection_pool.query('select id, title, body from posts')
|
|
364
|
+
|
|
365
|
+
end
|
|
366
|
+
end
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'helper'
|
|
4
4
|
|
|
5
|
-
class
|
|
5
|
+
class DBConnectionPoolTest < Minitest::Test
|
|
6
6
|
def setup
|
|
7
7
|
@machine = UM.new
|
|
8
8
|
@fn = "/tmp/#{rand(100000)}.db"
|
|
9
|
-
@cp = Syntropy::ConnectionPool.new(@machine, @fn, 4)
|
|
9
|
+
@cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
|
|
10
10
|
|
|
11
11
|
FileUtils.rm(@fn) rescue nil
|
|
12
12
|
@standalone_db = Extralite::Database.new(@fn)
|
|
@@ -14,6 +14,11 @@ class ConnectionPoolTest < Minitest::Test
|
|
|
14
14
|
@standalone_db.execute("insert into foo values (1, 2, 3)")
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def teardown
|
|
18
|
+
@standalone_db.close
|
|
19
|
+
@cp.close
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
def test_with_db
|
|
18
23
|
assert_equal 0, @cp.count
|
|
19
24
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helper'
|
|
4
|
+
|
|
5
|
+
class DBSchemaTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@machine = UM.new
|
|
8
|
+
@fn = "/tmp/#{rand(100000)}.db"
|
|
9
|
+
FileUtils.rm(@fn) rescue nil
|
|
10
|
+
@cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
@cp.close
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_db_schema_initial
|
|
18
|
+
schema = Syntropy::DB::Schema.new do
|
|
19
|
+
initial do |db|
|
|
20
|
+
db.execute <<~SQL
|
|
21
|
+
create table posts (
|
|
22
|
+
id integer primary key autoincrement,
|
|
23
|
+
title text,
|
|
24
|
+
body text
|
|
25
|
+
)
|
|
26
|
+
SQL
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
assert_nil schema.current_version(@cp)
|
|
31
|
+
schema.apply(@cp)
|
|
32
|
+
assert_equal '0000', schema.current_version(@cp)
|
|
33
|
+
|
|
34
|
+
assert_equal [], @cp.query('select id, title, body from posts')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_db_schema_version_blocks
|
|
38
|
+
schema = Syntropy::DB::Schema.new do
|
|
39
|
+
initial do |db|
|
|
40
|
+
db.execute <<~SQL
|
|
41
|
+
create table posts (
|
|
42
|
+
id integer primary key autoincrement,
|
|
43
|
+
title text,
|
|
44
|
+
body text
|
|
45
|
+
)
|
|
46
|
+
SQL
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
version('2026-05-30') do |db|
|
|
50
|
+
db.execute <<~SQL
|
|
51
|
+
insert into posts (title, body)
|
|
52
|
+
values ('foo', 'bar')
|
|
53
|
+
SQL
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
version('2026-05-31') do |db|
|
|
57
|
+
db.execute <<~SQL
|
|
58
|
+
update posts
|
|
59
|
+
set body = 'baz'
|
|
60
|
+
where title = 'foo'
|
|
61
|
+
SQL
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
assert_nil schema.current_version(@cp)
|
|
66
|
+
schema.apply(@cp)
|
|
67
|
+
assert_equal '2026-05-31', schema.current_version(@cp)
|
|
68
|
+
|
|
69
|
+
assert_equal [
|
|
70
|
+
{
|
|
71
|
+
id: 1,
|
|
72
|
+
title: 'foo',
|
|
73
|
+
body: 'baz'
|
|
74
|
+
}
|
|
75
|
+
], @cp.query('select id, title, body from posts')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_schema_from_module_files
|
|
79
|
+
module_loader = Syntropy::ModuleLoader.new({
|
|
80
|
+
root_dir: File.join(__dir__, 'schema')
|
|
81
|
+
})
|
|
82
|
+
schema = Syntropy::DB::Schema.new(module_loader:, schema_root: '/')
|
|
83
|
+
|
|
84
|
+
assert_nil schema.current_version(@cp)
|
|
85
|
+
schema.apply(@cp)
|
|
86
|
+
assert_equal '2026-05-30-bar', schema.current_version(@cp)
|
|
87
|
+
|
|
88
|
+
assert_equal [
|
|
89
|
+
{
|
|
90
|
+
id: 1,
|
|
91
|
+
title: 'foo',
|
|
92
|
+
body: 'baz'
|
|
93
|
+
}
|
|
94
|
+
], @cp.query('select id, title, body from posts')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helper'
|
|
4
|
+
|
|
5
|
+
class DBStoreTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@machine = UM.new
|
|
8
|
+
@fn = "/tmp/#{rand(100000)}.db"
|
|
9
|
+
FileUtils.rm(@fn) rescue nil
|
|
10
|
+
@cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
@cp.close
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_db_store
|
|
18
|
+
store = Syntropy::DB::Store.new(@cp)
|
|
19
|
+
|
|
20
|
+
assert_equal [{a: 42}], store.query("select ? as a", 42)
|
|
21
|
+
assert_equal({a: 42}, store.query_single_row("select ? as a", 42))
|
|
22
|
+
assert_equal 42, store.query_single_value("select ?", 42)
|
|
23
|
+
end
|
|
24
|
+
end
|