tep 0.11.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 +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Status codes, custom headers, content-type, redirect, halt.
|
|
4
|
+
class TestResponse < TepTest
|
|
5
|
+
app_source <<~RB
|
|
6
|
+
get '/ok' do
|
|
7
|
+
"ok"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
get '/created' do
|
|
11
|
+
status 201
|
|
12
|
+
"made"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
get '/no-content' do
|
|
16
|
+
status 204
|
|
17
|
+
""
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
get '/server-err' do
|
|
21
|
+
status 500
|
|
22
|
+
"boom"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
get '/plain' do
|
|
26
|
+
content_type 'text/plain; charset=utf-8'
|
|
27
|
+
"plain text"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
get '/json' do
|
|
31
|
+
content_type 'application/json'
|
|
32
|
+
'{"ok":true}'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
get '/custom-header' do
|
|
36
|
+
headers["X-Tep-Test"] = "yep"
|
|
37
|
+
"with header"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
get '/redirect' do
|
|
41
|
+
redirect '/ok'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
get '/redirect-301' do
|
|
45
|
+
redirect '/ok', 301
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
get '/halt-401' do
|
|
49
|
+
halt 401, "denied"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
get '/halt-bare' do
|
|
53
|
+
halt 418
|
|
54
|
+
end
|
|
55
|
+
RB
|
|
56
|
+
|
|
57
|
+
def test_default_200
|
|
58
|
+
res = get("/ok")
|
|
59
|
+
assert_equal "200", res.code
|
|
60
|
+
assert_equal "OK", res.message
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_status_201
|
|
64
|
+
assert_equal "201", get("/created").code
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_status_204
|
|
68
|
+
assert_equal "204", get("/no-content").code
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_status_500
|
|
72
|
+
assert_equal "500", get("/server-err").code
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_default_content_type_html
|
|
76
|
+
res = get("/ok")
|
|
77
|
+
assert_match(/text\/html/, res["content-type"])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_explicit_content_type_plain
|
|
81
|
+
res = get("/plain")
|
|
82
|
+
assert_equal "text/plain; charset=utf-8", res["content-type"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_explicit_content_type_json
|
|
86
|
+
res = get("/json")
|
|
87
|
+
assert_equal "application/json", res["content-type"]
|
|
88
|
+
assert_equal '{"ok":true}', res.body
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_custom_header
|
|
92
|
+
res = get("/custom-header")
|
|
93
|
+
assert_equal "yep", res["x-tep-test"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_redirect_default_302
|
|
97
|
+
res = get("/redirect")
|
|
98
|
+
assert_equal "302", res.code
|
|
99
|
+
assert_equal "/ok", res["location"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_redirect_explicit_301
|
|
103
|
+
res = get("/redirect-301")
|
|
104
|
+
assert_equal "301", res.code
|
|
105
|
+
assert_equal "/ok", res["location"]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_halt_with_body
|
|
109
|
+
res = get("/halt-401")
|
|
110
|
+
assert_equal "401", res.code
|
|
111
|
+
assert_equal "denied", res.body
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_halt_status_only
|
|
115
|
+
res = get("/halt-bare")
|
|
116
|
+
assert_equal "418", res.code
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_content_length
|
|
120
|
+
res = get("/ok")
|
|
121
|
+
assert_equal "2", res["content-length"]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Routing checklist: verbs, path params, query, splat, 404, method
|
|
4
|
+
# mismatch. Mirrors the surface in Sinatra's routing_test.rb that
|
|
5
|
+
# matches our v0.1 scope.
|
|
6
|
+
class TestRouting < TepTest
|
|
7
|
+
app_source <<~RB
|
|
8
|
+
get '/' do
|
|
9
|
+
"root"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
get '/hi/:name' do
|
|
13
|
+
"hi, " + params[:name]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
get '/users/:id/posts/:post_id' do
|
|
17
|
+
"user " + params[:id] + " post " + params[:post_id]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
get '/search' do
|
|
21
|
+
params[:q] + "/" + params[:page]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
get '/files/*' do
|
|
25
|
+
"splat"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
post '/echo' do
|
|
29
|
+
params[:msg]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
put '/widgets/:id' do
|
|
33
|
+
"put " + params[:id]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
patch '/widgets/:id' do
|
|
37
|
+
"patch " + params[:id]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
delete '/widgets/:id' do
|
|
41
|
+
"delete " + params[:id]
|
|
42
|
+
end
|
|
43
|
+
RB
|
|
44
|
+
|
|
45
|
+
def test_root
|
|
46
|
+
res = get("/")
|
|
47
|
+
assert_equal "200", res.code
|
|
48
|
+
assert_equal "root", res.body
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_path_param
|
|
52
|
+
res = get("/hi/world")
|
|
53
|
+
assert_equal "200", res.code
|
|
54
|
+
assert_equal "hi, world", res.body
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_two_path_params
|
|
58
|
+
res = get("/users/42/posts/7")
|
|
59
|
+
assert_equal "200", res.code
|
|
60
|
+
assert_equal "user 42 post 7", res.body
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_query_string
|
|
64
|
+
res = get("/search?q=ruby&page=2")
|
|
65
|
+
assert_equal "200", res.code
|
|
66
|
+
assert_equal "ruby/2", res.body
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_splat
|
|
70
|
+
res = get("/files/anything")
|
|
71
|
+
assert_equal "200", res.code
|
|
72
|
+
assert_equal "splat", res.body
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_post_form
|
|
76
|
+
res = post("/echo", "msg=hello",
|
|
77
|
+
"Content-Type" => "application/x-www-form-urlencoded")
|
|
78
|
+
assert_equal "200", res.code
|
|
79
|
+
assert_equal "hello", res.body
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_put
|
|
83
|
+
res = put("/widgets/1")
|
|
84
|
+
assert_equal "200", res.code
|
|
85
|
+
assert_equal "put 1", res.body
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_patch
|
|
89
|
+
res = patch("/widgets/1")
|
|
90
|
+
assert_equal "200", res.code
|
|
91
|
+
assert_equal "patch 1", res.body
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_delete
|
|
95
|
+
res = delete("/widgets/1")
|
|
96
|
+
assert_equal "200", res.code
|
|
97
|
+
assert_equal "delete 1", res.body
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_404_unknown_path
|
|
101
|
+
res = get("/nowhere")
|
|
102
|
+
assert_equal "404", res.code
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_404_method_mismatch
|
|
106
|
+
res = post("/", "")
|
|
107
|
+
assert_equal "404", res.code
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Scheduler -- cooperative fiber scheduler. End-to-end tests
|
|
4
|
+
# covering both parking modes:
|
|
5
|
+
#
|
|
6
|
+
# * Time-driven (run_until_empty / Fiber.yield)
|
|
7
|
+
# * I/O-driven (io_wait + poll(2)) -- a fiber parks on a listening
|
|
8
|
+
# socket, the handler issues an outbound connect against itself
|
|
9
|
+
# (sphttp_connect) to make the listener readable, the scheduler's
|
|
10
|
+
# next tick picks up the readiness and resumes the fiber.
|
|
11
|
+
class TestScheduler < TepTest
|
|
12
|
+
# The prefork handler in /cooperate SIGSEGVs on the
|
|
13
|
+
# Tep::Scheduler.clear + spawn_fiber + run_until_empty path.
|
|
14
|
+
# Verified still failing on current spinel master (post the
|
|
15
|
+
# #641 wave). Minimal repro pending -- the prefork-vs-cooperative
|
|
16
|
+
# crossover here doesn't reproduce from a 6-class fiber probe.
|
|
17
|
+
# Class stays skipped so the suite's signal stays useful.
|
|
18
|
+
def setup
|
|
19
|
+
skip "Tep::Scheduler primitives SIGSEGV in prefork handlers; " \
|
|
20
|
+
"minimal repro pending"
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
app_source <<~RB
|
|
25
|
+
require 'sinatra'
|
|
26
|
+
|
|
27
|
+
# Fiber body must be a method call on `self` (no closure over
|
|
28
|
+
# locals -- see spinel's test/fiber_yield_across_method_call.rb).
|
|
29
|
+
# So Worker stashes the Fiber in an ivar at construction time
|
|
30
|
+
# with an implicit-self body `run`, and the handler reads it
|
|
31
|
+
# back via `fiber`. Each tick appends `name + remaining` to
|
|
32
|
+
# `@trail` -- per-instance ivar, no class variables (mixing
|
|
33
|
+
# @@cvar mutation across a Fiber yield boundary tickled a
|
|
34
|
+
# spinel-side crash on Linux that didn't reproduce on macOS).
|
|
35
|
+
class Worker
|
|
36
|
+
attr_accessor :name, :remaining, :trail, :fiber
|
|
37
|
+
def initialize(n, count)
|
|
38
|
+
@name = n
|
|
39
|
+
@remaining = count
|
|
40
|
+
@trail = ""
|
|
41
|
+
@fiber = Fiber.new { run }
|
|
42
|
+
end
|
|
43
|
+
def run
|
|
44
|
+
while @remaining > 0
|
|
45
|
+
if @trail.length > 0
|
|
46
|
+
@trail = @trail + ","
|
|
47
|
+
end
|
|
48
|
+
@trail = @trail + @name + @remaining.to_s
|
|
49
|
+
@remaining -= 1
|
|
50
|
+
Fiber.yield
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
get '/cooperate' do
|
|
56
|
+
Tep::Scheduler.clear
|
|
57
|
+
w1 = Worker.new("A", 3)
|
|
58
|
+
Tep::Scheduler.spawn_fiber(w1.fiber)
|
|
59
|
+
n = Tep::Scheduler.run_until_empty
|
|
60
|
+
"loops=" + n.to_s + " trail=" + w1.trail
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
get '/alive' do
|
|
64
|
+
Tep::Scheduler.clear
|
|
65
|
+
before = Tep::Scheduler.alive_count.to_s
|
|
66
|
+
Tep::Scheduler.spawn_fiber(Tep.seed_fiber)
|
|
67
|
+
after = Tep::Scheduler.alive_count.to_s
|
|
68
|
+
before + "->" + after
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Park a fiber on a listening socket via io_wait. From outside the
|
|
72
|
+
# fiber the handler kicks an outbound TCP connect against the same
|
|
73
|
+
# listener -- now `lfd` is read-ready (pending accept), the next
|
|
74
|
+
# tick's poll(2) round sees POLLIN, and resumes the fiber with
|
|
75
|
+
# the ready bits.
|
|
76
|
+
class IoWorker
|
|
77
|
+
attr_accessor :result, :lfd, :timeout, :fiber
|
|
78
|
+
def initialize(lfd, timeout)
|
|
79
|
+
@lfd = lfd
|
|
80
|
+
@timeout = timeout
|
|
81
|
+
@result = -1
|
|
82
|
+
@fiber = Fiber.new { run }
|
|
83
|
+
end
|
|
84
|
+
def run
|
|
85
|
+
@result = Tep::Scheduler.io_wait(@lfd, Tep::Scheduler::READ, @timeout)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
get '/io_wait_ready' do
|
|
90
|
+
Tep::Scheduler.clear
|
|
91
|
+
lfd = Sock.sphttp_listen(15999, 0)
|
|
92
|
+
w = IoWorker.new(lfd, 3)
|
|
93
|
+
Tep::Scheduler.spawn_fiber(w.fiber)
|
|
94
|
+
cfd = Sock.sphttp_connect("127.0.0.1", 15999)
|
|
95
|
+
Tep::Scheduler.run_for(3)
|
|
96
|
+
Sock.sphttp_close(cfd)
|
|
97
|
+
Sock.sphttp_close(lfd)
|
|
98
|
+
"result=" + w.result.to_s + " cfd=" + (cfd > 0 ? "ok" : "fail")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
get '/io_wait_timeout' do
|
|
102
|
+
Tep::Scheduler.clear
|
|
103
|
+
lfd = Sock.sphttp_listen(15998, 0)
|
|
104
|
+
t0 = Time.now.to_i
|
|
105
|
+
w = IoWorker.new(lfd, 1)
|
|
106
|
+
Tep::Scheduler.spawn_fiber(w.fiber)
|
|
107
|
+
Tep::Scheduler.run_for(2)
|
|
108
|
+
elapsed = Time.now.to_i - t0
|
|
109
|
+
Sock.sphttp_close(lfd)
|
|
110
|
+
"result=" + w.result.to_s + " elapsed=" + elapsed.to_s
|
|
111
|
+
end
|
|
112
|
+
RB
|
|
113
|
+
|
|
114
|
+
def test_one_fiber_drains_via_run_until_empty
|
|
115
|
+
res = get("/cooperate")
|
|
116
|
+
assert_equal "200", res.code
|
|
117
|
+
body = res.body
|
|
118
|
+
# 3 iterations of `while @remaining > 0` produce 3 yields. Each
|
|
119
|
+
# yield gives back control after run_until_empty's tick. After
|
|
120
|
+
# the third yield the next resume re-enters the while header,
|
|
121
|
+
# the loop exits, and the fiber body returns -- that's a 4th
|
|
122
|
+
# successful resume before alive? flips to false. So loops=4.
|
|
123
|
+
assert_match(/loops=4/, body)
|
|
124
|
+
assert_match(/trail=A3,A2,A1/, body)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_alive_count_changes_as_fibers_spawn
|
|
128
|
+
res = get("/alive")
|
|
129
|
+
assert_equal "200", res.code
|
|
130
|
+
assert_equal "0->1", res.body.strip
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_io_wait_resumes_when_socket_becomes_readable
|
|
134
|
+
res = get("/io_wait_ready")
|
|
135
|
+
assert_equal "200", res.code
|
|
136
|
+
# READ bit is 1; the connect made the listener accept-ready, so
|
|
137
|
+
# the fiber should resume with result=1. cfd should be a real fd
|
|
138
|
+
# (> 0); if connect failed we'd see cfd=fail.
|
|
139
|
+
assert_match(/result=1/, res.body)
|
|
140
|
+
assert_match(/cfd=ok/, res.body)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_io_wait_returns_zero_on_timeout
|
|
144
|
+
res = get("/io_wait_timeout")
|
|
145
|
+
assert_equal "200", res.code
|
|
146
|
+
# No connect happens, so the listener never becomes readable.
|
|
147
|
+
# After ~1s the timeout fires and io_wait returns 0.
|
|
148
|
+
assert_match(/result=0/, res.body)
|
|
149
|
+
# 1s timeout, run_for cap 2s, allow either 1 or 2 elapsed seconds
|
|
150
|
+
# (poll wake-up + clock granularity).
|
|
151
|
+
assert_match(/elapsed=[12]/, res.body)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Security::Cors + Tep::Security::Headers -- before / after
|
|
4
|
+
# filter classes that wire into Tep.before / Tep.after.
|
|
5
|
+
class TestSecurity < TepTest
|
|
6
|
+
app_source <<~RB
|
|
7
|
+
require 'sinatra'
|
|
8
|
+
|
|
9
|
+
CORS = Tep::Security::Cors.new
|
|
10
|
+
CORS.set_origin("https://app.example.com")
|
|
11
|
+
CORS.set_allowed_verbs("GET,POST,DELETE,OPTIONS")
|
|
12
|
+
CORS.set_allowed_headers("Content-Type,X-Custom")
|
|
13
|
+
CORS.set_max_age(7200)
|
|
14
|
+
Tep.before CORS
|
|
15
|
+
|
|
16
|
+
HEADERS = Tep::Security::Headers.new
|
|
17
|
+
HEADERS.set_hsts(31536000) # 1 year
|
|
18
|
+
Tep.after HEADERS
|
|
19
|
+
|
|
20
|
+
get '/' do
|
|
21
|
+
"ok"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Route that pre-sets X-Frame-Options before the Headers
|
|
25
|
+
# after-filter runs -- verifies the filter's
|
|
26
|
+
# `unless res.headers.key?` guard preserves handler-set values.
|
|
27
|
+
get '/custom_frame' do
|
|
28
|
+
res.headers["X-Frame-Options"] = "DENY"
|
|
29
|
+
"ok"
|
|
30
|
+
end
|
|
31
|
+
RB
|
|
32
|
+
|
|
33
|
+
def test_cors_origin_on_get
|
|
34
|
+
res = get("/")
|
|
35
|
+
assert_equal "200", res.code
|
|
36
|
+
assert_equal "https://app.example.com", res["Access-Control-Allow-Origin"]
|
|
37
|
+
assert_equal "Origin", res["Vary"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_cors_preflight_returns_204
|
|
41
|
+
res = req(:options, "/", nil, {})
|
|
42
|
+
assert_equal "204", res.code
|
|
43
|
+
assert_equal "GET,POST,DELETE,OPTIONS", res["Access-Control-Allow-Methods"]
|
|
44
|
+
assert_equal "Content-Type,X-Custom", res["Access-Control-Allow-Headers"]
|
|
45
|
+
assert_equal "7200", res["Access-Control-Max-Age"]
|
|
46
|
+
assert_equal "https://app.example.com", res["Access-Control-Allow-Origin"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_default_secure_headers_on_response
|
|
50
|
+
res = get("/")
|
|
51
|
+
assert_equal "nosniff", res["X-Content-Type-Options"]
|
|
52
|
+
assert_equal "SAMEORIGIN", res["X-Frame-Options"]
|
|
53
|
+
assert_equal "strict-origin-when-cross-origin", res["Referrer-Policy"]
|
|
54
|
+
assert_equal "0", res["X-XSS-Protection"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_hsts_when_configured
|
|
58
|
+
res = get("/")
|
|
59
|
+
assert_match(/max-age=31536000/, res["Strict-Transport-Security"])
|
|
60
|
+
assert_match(/includeSubDomains/, res["Strict-Transport-Security"])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_handler_set_header_survives_after_filter
|
|
64
|
+
# The Headers after-filter only sets each header when not already
|
|
65
|
+
# present (`unless res.headers.key?`). /custom_frame pre-sets
|
|
66
|
+
# X-Frame-Options to DENY; the filter must not overwrite it back
|
|
67
|
+
# to SAMEORIGIN.
|
|
68
|
+
res = get("/custom_frame")
|
|
69
|
+
assert_equal "200", res.code
|
|
70
|
+
assert_equal "DENY", res["X-Frame-Options"]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Smoke test for Tep::Server::Scheduled (fiber-per-connection server).
|
|
2
|
+
# Validates the basic HTTP request/response loop works under the
|
|
3
|
+
# scheduler. Concurrent-connection / slow-client cases are out of
|
|
4
|
+
# scope for this initial test -- they'll get their own coverage when
|
|
5
|
+
# the WebSocket battery lands and exercises that surface for real.
|
|
6
|
+
require_relative "helper"
|
|
7
|
+
|
|
8
|
+
class TestServerScheduled < TepTest
|
|
9
|
+
app_source <<~RB
|
|
10
|
+
require "sinatra"
|
|
11
|
+
|
|
12
|
+
set :scheduler, :scheduled
|
|
13
|
+
|
|
14
|
+
get "/ping" do
|
|
15
|
+
"pong"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
get "/echo/:word" do
|
|
19
|
+
params[:word]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
post "/upper" do
|
|
23
|
+
request.body.upcase
|
|
24
|
+
end
|
|
25
|
+
RB
|
|
26
|
+
|
|
27
|
+
def test_basic_get_through_scheduler
|
|
28
|
+
res = get("/ping")
|
|
29
|
+
assert_equal "200", res.code
|
|
30
|
+
assert_equal "pong", res.body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_path_capture_through_scheduler
|
|
34
|
+
res = get("/echo/hello")
|
|
35
|
+
assert_equal "200", res.code
|
|
36
|
+
assert_equal "hello", res.body
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_post_body_through_scheduler
|
|
40
|
+
res = post("/upper", "hello world")
|
|
41
|
+
assert_equal "200", res.code
|
|
42
|
+
assert_equal "HELLO WORLD", res.body
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_two_sequential_requests
|
|
46
|
+
# Same connection, two HTTP/1.1 keep-alive requests. The
|
|
47
|
+
# scheduler's per-connection-fiber must handle the second
|
|
48
|
+
# iteration of its keep-alive loop correctly.
|
|
49
|
+
r1 = get("/ping")
|
|
50
|
+
r2 = get("/echo/two")
|
|
51
|
+
assert_equal "200", r1.code
|
|
52
|
+
assert_equal "200", r2.code
|
|
53
|
+
assert_equal "pong", r1.body
|
|
54
|
+
assert_equal "two", r2.body
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestSessions < TepTest
|
|
4
|
+
app_source <<~RB
|
|
5
|
+
Tep.session_secret = "test-secret-1234567890abcdef"
|
|
6
|
+
|
|
7
|
+
get '/login' do
|
|
8
|
+
session["user"] = "alice"
|
|
9
|
+
session["plan"] = "pro"
|
|
10
|
+
"logged in"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
get '/whoami' do
|
|
14
|
+
"user=" + session["user"] + " plan=" + session["plan"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
get '/no-session-write' do
|
|
18
|
+
"ok"
|
|
19
|
+
end
|
|
20
|
+
RB
|
|
21
|
+
|
|
22
|
+
def test_login_sets_signed_cookie
|
|
23
|
+
res = get("/login")
|
|
24
|
+
assert_equal "200", res.code
|
|
25
|
+
assert_match(/^tep\.session=/, res["set-cookie"])
|
|
26
|
+
assert_match(/HttpOnly/, res["set-cookie"])
|
|
27
|
+
assert_match(/SameSite=Lax/, res["set-cookie"])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_session_round_trip
|
|
31
|
+
login = get("/login")
|
|
32
|
+
cookie_line = login["set-cookie"]
|
|
33
|
+
# Extract the Cookie name=value before the first ';'.
|
|
34
|
+
cookie_pair = cookie_line.split(";").first
|
|
35
|
+
res = get("/whoami", "Cookie" => cookie_pair)
|
|
36
|
+
assert_equal "200", res.code
|
|
37
|
+
assert_equal "user=alice plan=pro", res.body
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_no_write_no_set_cookie
|
|
41
|
+
res = get("/no-session-write")
|
|
42
|
+
assert_equal "200", res.code
|
|
43
|
+
assert_nil res["set-cookie"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_tampered_cookie_rejected
|
|
47
|
+
# Take a real session cookie, corrupt the signature, and expect
|
|
48
|
+
# whoami to see no session data.
|
|
49
|
+
login = get("/login")
|
|
50
|
+
line = login["set-cookie"]
|
|
51
|
+
pair = line.split(";").first
|
|
52
|
+
name, val = pair.split("=", 2)
|
|
53
|
+
# Flip a bit in the signature (last char): should fail HMAC verify.
|
|
54
|
+
tampered = val[0...-1] + (val[-1] == "0" ? "1" : "0")
|
|
55
|
+
res = get("/whoami", "Cookie" => "#{name}=#{tampered}")
|
|
56
|
+
# Session is rejected -> empty values for both keys.
|
|
57
|
+
assert_equal "user= plan=", res.body
|
|
58
|
+
end
|
|
59
|
+
end
|
data/test/test_shell.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Shell -- popen + read. Lives behind FFI helpers
|
|
4
|
+
# (sphttp_shell_capture, sphttp_file_read); this test boots a tiny
|
|
5
|
+
# tep app that exercises both from inside a handler.
|
|
6
|
+
class TestShell < TepTest
|
|
7
|
+
app_source <<~RB
|
|
8
|
+
require 'sinatra'
|
|
9
|
+
|
|
10
|
+
get '/echo' do
|
|
11
|
+
Tep::Shell.run("printf hello").strip
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
get '/run_limited' do
|
|
15
|
+
# Cap at 3 bytes; full output is "hello" -- we should see "hel".
|
|
16
|
+
Tep::Shell.run_limited("printf hello", 3)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
get '/file' do
|
|
20
|
+
# /etc/hosts ships on Linux + macOS + BSDs; /etc/hostname is
|
|
21
|
+
# Linux-only (macOS keeps the hostname in scutil instead).
|
|
22
|
+
Tep::Shell.read("/etc/hosts").length.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
get '/missing' do
|
|
26
|
+
Tep::Shell.read("/does/not/exist/at/all").length.to_s
|
|
27
|
+
end
|
|
28
|
+
RB
|
|
29
|
+
|
|
30
|
+
def test_run_captures_stdout
|
|
31
|
+
res = get("/echo")
|
|
32
|
+
assert_equal "200", res.code
|
|
33
|
+
assert_equal "hello", res.body.strip
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_run_respects_byte_cap
|
|
37
|
+
res = get("/run_limited")
|
|
38
|
+
assert_equal "200", res.code
|
|
39
|
+
assert_equal "hel", res.body
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_read_returns_bytes_for_real_file
|
|
43
|
+
res = get("/file")
|
|
44
|
+
assert_equal "200", res.code
|
|
45
|
+
# /etc/hosts is always non-empty on any sane Unix.
|
|
46
|
+
assert_operator res.body.strip.to_i, :>=, 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_read_returns_empty_on_missing_path
|
|
50
|
+
res = get("/missing")
|
|
51
|
+
assert_equal "200", res.code
|
|
52
|
+
assert_equal "0", res.body.strip
|
|
53
|
+
end
|
|
54
|
+
end
|