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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
@@ -0,0 +1,148 @@
1
+ require_relative "helper"
2
+
3
+ # Tep::SQLite -- a thin libsqlite3 binding wired through spinel's
4
+ # FFI DSL. Tests cover: basic CRUD, parameterised insert + select,
5
+ # multi-row iteration, last_rowid, and a per-test temp .db file
6
+ # so test order is irrelevant.
7
+ class TestSqlite < TepTest
8
+ TMP_DB = "/tmp/tep_test_#{$$}.db"
9
+
10
+ app_source <<~RB
11
+ require 'sinatra'
12
+
13
+ on_start do
14
+ db = Tep::SQLite.new
15
+ if db.open("#{TMP_DB}")
16
+ db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
17
+ db.exec("DELETE FROM notes")
18
+ db.prepare("INSERT INTO notes (body) VALUES (?)")
19
+ db.bind_str(1, "first note")
20
+ db.step
21
+ db.reset
22
+ db.bind_str(1, "second note")
23
+ db.step
24
+ db.reset
25
+ db.bind_str(1, "third note")
26
+ db.step
27
+ db.finalize
28
+ db.close
29
+ end
30
+ end
31
+
32
+ get '/note/:id' do
33
+ db = Tep::SQLite.new
34
+ db.open("#{TMP_DB}")
35
+ body = db.first_str("SELECT body FROM notes WHERE id = ?", params[:id])
36
+ db.close
37
+ "id=" + params[:id] + " body=" + body
38
+ end
39
+
40
+ get '/notes' do
41
+ db = Tep::SQLite.new
42
+ db.open("#{TMP_DB}")
43
+ out = ""
44
+ db.prepare("SELECT id, body FROM notes ORDER BY id")
45
+ while db.step == 1
46
+ out = out + db.col_int(0).to_s + ":" + db.col_str(1) + "\\n"
47
+ end
48
+ db.finalize
49
+ db.close
50
+ out
51
+ end
52
+
53
+ get '/count' do
54
+ db = Tep::SQLite.new
55
+ db.open("#{TMP_DB}")
56
+ n = db.first_int("SELECT count(*) FROM notes", "")
57
+ db.close
58
+ "count=" + n.to_s
59
+ end
60
+
61
+ post '/notes' do
62
+ db = Tep::SQLite.new
63
+ db.open("#{TMP_DB}")
64
+ db.prepare("INSERT INTO notes (body) VALUES (?)")
65
+ db.bind_str(1, params[:body])
66
+ db.step
67
+ db.finalize
68
+ id = db.last_rowid
69
+ db.close
70
+ "inserted=" + id.to_s
71
+ end
72
+
73
+ # 64-bit round-trip (issue #171): bind a value > 2^31 via bind_int,
74
+ # read it back via col_int. With the old 32-bit path this wrapped
75
+ # negative (3427544687 -> -867422609); now it must survive intact.
76
+ get '/bigint' do
77
+ db = Tep::SQLite.new
78
+ db.open("#{TMP_DB}")
79
+ db.exec("CREATE TABLE IF NOT EXISTS bignums (n INTEGER)")
80
+ db.exec("DELETE FROM bignums")
81
+ db.prepare("INSERT INTO bignums (n) VALUES (?)")
82
+ db.bind_int(1, 3427544687)
83
+ db.step
84
+ db.finalize
85
+ db.prepare("SELECT n FROM bignums LIMIT 1")
86
+ db.step
87
+ n = db.col_int(0)
88
+ db.finalize
89
+ db.close
90
+ "n=" + n.to_s
91
+ end
92
+ RB
93
+
94
+ Minitest.after_run do
95
+ File.unlink(TMP_DB) if File.exist?(TMP_DB)
96
+ end
97
+
98
+ def test_first_str_with_param
99
+ res = get("/note/1")
100
+ assert_equal "200", res.code
101
+ assert_equal "id=1 body=first note", res.body.strip
102
+ end
103
+
104
+ def test_first_str_missing_row_returns_empty
105
+ res = get("/note/9999")
106
+ assert_equal "200", res.code
107
+ assert_equal "id=9999 body=", res.body.strip
108
+ end
109
+
110
+ # Tests that mutate are written to be order-independent: minitest
111
+ # randomises seed-shuffled order and the test app boots once per
112
+ # class (on_start runs once), so reads-then-writes-then-reads in
113
+ # an arbitrary order all need to make sense.
114
+
115
+ def test_iterate_all_rows_has_seeded_bodies
116
+ res = get("/notes")
117
+ assert_equal "200", res.code
118
+ body = res.body
119
+ assert_match(/:first note$/, body)
120
+ assert_match(/:second note$/, body)
121
+ assert_match(/:third note$/, body)
122
+ end
123
+
124
+ def test_first_int_returns_at_least_seed_count
125
+ res = get("/count")
126
+ assert_equal "200", res.code
127
+ n = res.body.strip.split("=").last.to_i
128
+ assert n >= 3, "expected count >= 3, got #{n}"
129
+ end
130
+
131
+ def test_insert_and_last_rowid_round_trips
132
+ res = post("/notes", "body=via-test")
133
+ assert_equal "200", res.code
134
+ inserted_id = res.body.strip.split("=").last.to_i
135
+ assert inserted_id >= 1, "expected a positive rowid, got #{inserted_id}"
136
+
137
+ res2 = get("/note/#{inserted_id}")
138
+ assert_match(/body=via-test/, res2.body)
139
+ end
140
+
141
+ def test_bind_and_col_int_round_trip_64bit
142
+ # 3,427,544,687 > 2^31-1. The old 32-bit bind/col path truncated it
143
+ # to -867,422,609 (issue #171); 64-bit bind_int/col_int preserve it.
144
+ res = get("/bigint")
145
+ assert_equal "200", res.code
146
+ assert_equal "n=3427544687", res.body
147
+ end
148
+ end
@@ -0,0 +1,171 @@
1
+ require_relative "helper"
2
+
3
+ # Tep::SQLite#prepare_cached -- cache hit / miss / reset-on-finalize
4
+ # semantics. Backs the perf-leverage win documented in issue #75.
5
+ class TestSqliteCached < TepTest
6
+ TMP_DB = "/tmp/tep_test_cache_#{$$}.db"
7
+
8
+ app_source <<~RB
9
+ require 'sinatra'
10
+
11
+ on_start do
12
+ db = Tep::SQLite.new
13
+ if db.open("#{TMP_DB}")
14
+ db.exec("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)")
15
+ db.exec("DELETE FROM items")
16
+ db.prepare("INSERT INTO items (name) VALUES (?)")
17
+ db.bind_str(1, "alpha")
18
+ db.step
19
+ db.reset
20
+ db.bind_str(1, "beta")
21
+ db.step
22
+ db.reset
23
+ db.bind_str(1, "gamma")
24
+ db.step
25
+ db.finalize
26
+ db.close
27
+ end
28
+ end
29
+
30
+ # Repeated prepare_cached with the SAME sql + DIFFERENT
31
+ # bindings -- the cache reuse path must reset+clear_bindings
32
+ # so the second call's binding wins.
33
+ get '/cached_rebind' do
34
+ db = Tep::SQLite.new
35
+ db.open("#{TMP_DB}")
36
+ sql = "SELECT name FROM items WHERE id = ?"
37
+ out = ""
38
+ i = 1
39
+ while i <= 3
40
+ db.prepare_cached(sql)
41
+ db.bind_int(1, i)
42
+ if db.step == 1
43
+ if out.length > 0
44
+ out = out + ","
45
+ end
46
+ out = out + db.col_str(0)
47
+ end
48
+ db.finalize
49
+ i += 1
50
+ end
51
+ db.close
52
+ out
53
+ end
54
+
55
+ # Mix prepare + prepare_cached + prepare -- exercises both
56
+ # paths in alternation. Uncached prepare after a cached one
57
+ # must release the cached cursor via reset (NOT finalize),
58
+ # leaving the cache slot valid for the next prepare_cached.
59
+ get '/mixed_prepare' do
60
+ db = Tep::SQLite.new
61
+ db.open("#{TMP_DB}")
62
+ cached_sql = "SELECT name FROM items WHERE id = ?"
63
+ # First cached hit (cache miss -> populate slot)
64
+ db.prepare_cached(cached_sql)
65
+ db.bind_int(1, 1)
66
+ db.step
67
+ first = db.col_str(0)
68
+ db.finalize
69
+ # Uncached prepare in between
70
+ db.prepare("SELECT name FROM items WHERE id = ?")
71
+ db.bind_int(1, 2)
72
+ db.step
73
+ middle = db.col_str(0)
74
+ db.finalize
75
+ # Cached hit again (slot still alive)
76
+ db.prepare_cached(cached_sql)
77
+ db.bind_int(1, 3)
78
+ db.step
79
+ last = db.col_str(0)
80
+ db.finalize
81
+ db.close
82
+ first + "|" + middle + "|" + last
83
+ end
84
+
85
+ # Cache survives across a different db (per-handle scope) --
86
+ # the cached_sql here belongs to db1; db2 with the same SQL
87
+ # gets a separate cache slot.
88
+ get '/per_handle' do
89
+ sql = "SELECT name FROM items WHERE id = ?"
90
+ db1 = Tep::SQLite.new
91
+ db1.open("#{TMP_DB}")
92
+ db1.prepare_cached(sql)
93
+ db1.bind_int(1, 1)
94
+ db1.step
95
+ r1 = db1.col_str(0)
96
+ db1.finalize
97
+ db1.close # close while a cache slot still exists for this handle
98
+
99
+ db2 = Tep::SQLite.new
100
+ db2.open("#{TMP_DB}")
101
+ db2.prepare_cached(sql)
102
+ db2.bind_int(1, 2)
103
+ db2.step
104
+ r2 = db2.col_str(0)
105
+ db2.finalize
106
+ db2.close
107
+ r1 + "|" + r2
108
+ end
109
+
110
+ # Smoke: a tight loop of cached calls works without leaks --
111
+ # exercises the cache hit path repeatedly with the same SQL.
112
+ get '/loop_count' do
113
+ db = Tep::SQLite.new
114
+ db.open("#{TMP_DB}")
115
+ sql = "SELECT count(*) FROM items"
116
+ n = 0
117
+ i = 0
118
+ while i < 50
119
+ db.prepare_cached(sql)
120
+ if db.step == 1
121
+ n = db.col_int(0)
122
+ end
123
+ db.finalize
124
+ i += 1
125
+ end
126
+ db.close
127
+ "loops=" + i.to_s + " count=" + n.to_s
128
+ end
129
+ RB
130
+
131
+ Minitest.after_run do
132
+ File.unlink(TMP_DB) if File.exist?(TMP_DB)
133
+ end
134
+
135
+ def test_cache_hit_rebinds_correctly
136
+ # Same SQL, three different bindings, three different rows.
137
+ # If clear_bindings didn't fire, the int param would leak
138
+ # across calls and rows would repeat.
139
+ res = get("/cached_rebind")
140
+ assert_equal "200", res.code
141
+ assert_equal "alpha,beta,gamma", res.body
142
+ end
143
+
144
+ def test_mixed_prepare_paths_coexist
145
+ # The uncached prepare in the middle must not invalidate the
146
+ # cached slot for the surrounding prepare_cached calls.
147
+ res = get("/mixed_prepare")
148
+ assert_equal "200", res.code
149
+ assert_equal "alpha|beta|gamma", res.body
150
+ end
151
+
152
+ def test_close_finalizes_cached_stmts_for_handle
153
+ # /per_handle opens db1, caches a stmt, closes db1, opens db2,
154
+ # caches the same SQL (separate slot), closes db2. If close()
155
+ # didn't finalize db1's cached stmt, sqlite3_close would
156
+ # SQLITE_BUSY and the second open would land an invalid handle.
157
+ res = get("/per_handle")
158
+ assert_equal "200", res.code
159
+ assert_equal "alpha|beta", res.body
160
+ end
161
+
162
+ def test_loop_of_cached_calls_no_leak
163
+ # 50 reuses of the same cached SQL. If the cache flag handling
164
+ # was wrong, sqlite would either leak or error out around
165
+ # iteration 16+ (SQLITE_MAX_STATEMENT_PARAMETERS / stack
166
+ # exhaustion).
167
+ res = get("/loop_count")
168
+ assert_equal "200", res.code
169
+ assert_match(/loops=50 count=3/, res.body)
170
+ end
171
+ end
@@ -0,0 +1,57 @@
1
+ require_relative "helper"
2
+
3
+ # Static file serving + custom 404 handler.
4
+ class TestStatic < TepTest
5
+ app_source <<~RB
6
+ set :public_dir, '#{File.expand_path("../public", __dir__)}'
7
+
8
+ not_found do
9
+ "tep-404 " + request.path
10
+ end
11
+
12
+ get '/' do
13
+ "root"
14
+ end
15
+ RB
16
+
17
+ def test_static_text_file
18
+ res = get("/hello.txt")
19
+ assert_equal "200", res.code
20
+ assert_match(/text\/plain/, res["content-type"])
21
+ assert_match(/static file serving/, res.body)
22
+ end
23
+
24
+ def test_static_css
25
+ res = get("/style.css")
26
+ assert_equal "200", res.code
27
+ assert_equal "text/css", res["content-type"]
28
+ end
29
+
30
+ def test_static_x_tep_marker
31
+ res = get("/hello.txt")
32
+ assert_equal "1", res["x-tep-static"]
33
+ end
34
+
35
+ def test_404_for_unknown_path
36
+ res = get("/no-such-file.txt")
37
+ assert_equal "404", res.code
38
+ end
39
+
40
+ def test_custom_404_body
41
+ res = get("/no-such-file.txt")
42
+ assert_match(/tep-404/, res.body)
43
+ assert_match(%r{/no-such-file\.txt}, res.body)
44
+ end
45
+
46
+ def test_path_traversal_rejected
47
+ res = get("/../etc/passwd")
48
+ assert_equal "404", res.code
49
+ refute_match(/root:/, res.body)
50
+ end
51
+
52
+ def test_get_route_still_wins_over_static
53
+ res = get("/")
54
+ assert_equal "200", res.code
55
+ assert_equal "root", res.body
56
+ end
57
+ end
@@ -0,0 +1,96 @@
1
+ require_relative "helper"
2
+
3
+ class TestStreaming < TepTest
4
+ app_source <<~RB
5
+ require 'sinatra'
6
+
7
+ class Ticks < Tep::Streamer
8
+ def pump(out)
9
+ out.write("data: 1\\n\\n")
10
+ out.write("data: 2\\n\\n")
11
+ out.write("data: 3\\n\\n")
12
+ end
13
+ end
14
+
15
+ get '/stream' do
16
+ stream Ticks.new
17
+ end
18
+
19
+ get '/normal' do
20
+ "regular response"
21
+ end
22
+ RB
23
+
24
+ def test_stream_uses_chunked_encoding
25
+ res = get("/stream")
26
+ assert_equal "200", res.code
27
+ assert_equal "chunked", res["transfer-encoding"]
28
+ assert_nil res["content-length"]
29
+ end
30
+
31
+ def test_stream_default_content_type
32
+ res = get("/stream")
33
+ assert_match(%r{text/event-stream}, res["content-type"])
34
+ end
35
+
36
+ def test_stream_emits_all_chunks
37
+ res = get("/stream")
38
+ assert_equal "data: 1\n\ndata: 2\n\ndata: 3\n\n", res.body
39
+ end
40
+
41
+ def test_normal_route_still_buffered
42
+ res = get("/normal")
43
+ assert_equal "200", res.code
44
+ assert_nil res["transfer-encoding"]
45
+ assert_equal "16", res["content-length"]
46
+ assert_equal "regular response", res.body
47
+ end
48
+ end
49
+
50
+ # Same streaming surface under Tep::Server::Scheduled. Regression
51
+ # guard for #90: the scheduled server's write_response had no
52
+ # res.streaming branch, so streamed responses came back with
53
+ # Content-Length: 0 and an empty body (the Streamer never pumped).
54
+ class TestStreamingScheduled < TepTest
55
+ app_source <<~RB
56
+ require 'sinatra'
57
+
58
+ set :scheduler, :scheduled
59
+ set :workers, 1
60
+
61
+ class Ticks < Tep::Streamer
62
+ def pump(out)
63
+ out.write("data: 1\\n\\n")
64
+ out.write("data: 2\\n\\n")
65
+ out.write("data: 3\\n\\n")
66
+ end
67
+ end
68
+
69
+ get '/stream' do
70
+ stream Ticks.new
71
+ end
72
+
73
+ get '/normal' do
74
+ "regular response"
75
+ end
76
+ RB
77
+
78
+ def test_scheduled_stream_uses_chunked_encoding
79
+ res = get("/stream")
80
+ assert_equal "200", res.code
81
+ assert_equal "chunked", res["transfer-encoding"]
82
+ assert_nil res["content-length"]
83
+ end
84
+
85
+ def test_scheduled_stream_emits_all_chunks
86
+ res = get("/stream")
87
+ assert_equal "data: 1\n\ndata: 2\n\ndata: 3\n\n", res.body
88
+ end
89
+
90
+ def test_scheduled_normal_route_still_buffered
91
+ res = get("/normal")
92
+ assert_equal "200", res.code
93
+ assert_nil res["transfer-encoding"]
94
+ assert_equal "regular response", res.body
95
+ end
96
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "helper"
2
+
3
+ # Features Sinatra has and tep doesn't yet. Tests that have moved
4
+ # into the supported column live in their own file (test_cookies.rb,
5
+ # test_sessions.rb, test_streaming.rb, test_regex_routes.rb,
6
+ # test_modular.rb, test_erb.rb, test_misc_v02.rb, test_pass.rb,
7
+ # test_multi_filters.rb, test_optional_segments.rb,
8
+ # test_request_methods.rb).
9
+ class TestUnsupported < TepTest
10
+ app_source <<~RB
11
+ get '/' do
12
+ "tep skip stub"
13
+ end
14
+ RB
15
+
16
+ def test_haml_templates
17
+ skip "Haml -- depends on a gem; out of scope for spinel-AOT"
18
+ end
19
+
20
+ def test_helpers_block
21
+ skip "`helpers do ... end` -- closures not first-class in spinel; would need translator support to define methods on Tep::Handler"
22
+ end
23
+
24
+ def test_request_ip
25
+ skip "request.ip / request.remote_ip -- needs an sphttp_accept_with_peer C helper. Other Rack::Request methods (host, user_agent, scheme, ssl?, etc.) are supported -- see test_request_methods.rb."
26
+ end
27
+
28
+ # `@ivar` template locals are now supported -- see test_erb_ivars.rb.
29
+
30
+ # send_file, configure, pass, multiple filters, optional segments
31
+ # have all moved into supported and have their own test files.
32
+ end
@@ -0,0 +1,152 @@
1
+ # Tep::WebSocket frame + handshake tests via a live tep app.
2
+ # RFC 6455 reference vectors covered:
3
+ # - Frame encode for text + binary + close + ping + pong
4
+ # - Handshake accept-key compute (the §1.3 worked example)
5
+ # - Handshake header parsing (Upgrade / Connection / Version / Key)
6
+ # - Subprotocol negotiation parser
7
+ #
8
+ # Integration coverage (Driver + Connection over a real socket
9
+ # round-trip) lives separately and isn't in this commit -- it needs
10
+ # a Ruby-side WS client to wire up, which is its own dependency.
11
+ require_relative "helper"
12
+
13
+ class TestWebSocket < TepTest
14
+ app_source <<~'RB'
15
+ require "sinatra"
16
+
17
+ # Encode a small text frame and dump it as a hex string so the
18
+ # MRI test can check byte-by-byte. Server frames are unmasked
19
+ # so byte 0 = 0x81 (FIN + opcode 1), byte 1 = payload length.
20
+ def hex_of(s)
21
+ out = ""
22
+ i = 0
23
+ while i < s.length
24
+ b = s[i].ord & 0xff
25
+ out = out + ((b / 16) < 10 ? (b / 16 + 48).chr : (b / 16 + 87).chr)
26
+ out = out + ((b % 16) < 10 ? (b % 16 + 48).chr : (b % 16 + 87).chr)
27
+ i += 1
28
+ end
29
+ out
30
+ end
31
+
32
+ get '/frame/text_small' do
33
+ f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_TEXT, "Hello")
34
+ hex_of(f.encode_unmasked)
35
+ end
36
+
37
+ get '/frame/binary_short' do
38
+ f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_BINARY, "abc")
39
+ hex_of(f.encode_unmasked)
40
+ end
41
+
42
+ get '/frame/text_extended_16' do
43
+ payload = ""
44
+ i = 0
45
+ while i < 200
46
+ payload = payload + "x"
47
+ i += 1
48
+ end
49
+ f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_TEXT, payload)
50
+ # First 4 bytes only -- enough to verify the 16-bit length
51
+ # encoding (0x81 0x7e 0x00 0xc8 = FIN+text, 126 marker, 200 in 16-bit).
52
+ hex_of(f.encode_unmasked[0, 4])
53
+ end
54
+
55
+ get '/frame/close_with_code' do
56
+ body = Tep::WebSocket::Driver.encode_close_payload(1000, "bye")
57
+ f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_CLOSE, body)
58
+ hex_of(f.encode_unmasked)
59
+ end
60
+
61
+ # Handshake accept key for the RFC 6455 §1.3 worked example.
62
+ get '/handshake/accept_key' do
63
+ Crypto.sp_crypto_websocket_accept("dGhlIHNhbXBsZSBub25jZQ==")
64
+ end
65
+
66
+ get '/handshake/build_response_with_protocol' do
67
+ Tep::WebSocket::Handshake.build_response("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "chat")
68
+ end
69
+
70
+ get '/handshake/build_response_no_protocol' do
71
+ Tep::WebSocket::Handshake.build_response("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "")
72
+ end
73
+
74
+ get '/handshake/split_csv' do
75
+ parts = Tep::WebSocket::Handshake.split_csv("a, b ,c")
76
+ parts.join("|")
77
+ end
78
+
79
+ get '/frame/reserved_opcode_rejects' do
80
+ Tep::WebSocket::Frame.reserved_opcode?(5) ? "yes" : "no"
81
+ end
82
+
83
+ get '/frame/control_opcode_classifies' do
84
+ a = Tep::WebSocket::Frame.control_opcode?(Tep::WebSocket::OPCODE_CLOSE) ? "y" : "n"
85
+ b = Tep::WebSocket::Frame.control_opcode?(Tep::WebSocket::OPCODE_TEXT) ? "y" : "n"
86
+ a + b
87
+ end
88
+ RB
89
+
90
+ def test_frame_text_small_encode
91
+ res = get("/frame/text_small")
92
+ # 0x81 = FIN + opcode 1 (text), 0x05 = unmasked, 5-byte payload,
93
+ # then "Hello" = 48 65 6c 6c 6f.
94
+ assert_equal "8105" + "48656c6c6f", res.body
95
+ end
96
+
97
+ def test_frame_binary_short_encode
98
+ res = get("/frame/binary_short")
99
+ # 0x82 = FIN + opcode 2 (binary), 0x03 = unmasked 3-byte payload,
100
+ # then "abc" = 61 62 63.
101
+ assert_equal "8203" + "616263", res.body
102
+ end
103
+
104
+ def test_frame_16bit_length_marker
105
+ res = get("/frame/text_extended_16")
106
+ # 0x81 = FIN + text, 0x7e = 126 marker (16-bit length follows),
107
+ # 0x00 0xc8 = 200 big-endian.
108
+ assert_equal "817e00c8", res.body
109
+ end
110
+
111
+ def test_frame_close_with_code_and_reason
112
+ res = get("/frame/close_with_code")
113
+ # 0x88 = FIN + opcode 8 (close), 0x05 = 5-byte payload,
114
+ # 0x03 0xe8 = 1000 big-endian, then "bye" = 62 79 65.
115
+ assert_equal "8805" + "03e8" + "627965", res.body
116
+ end
117
+
118
+ def test_handshake_accept_key_rfc_6455_vector
119
+ res = get("/handshake/accept_key")
120
+ # RFC 6455 §1.3 worked example.
121
+ assert_equal "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", res.body
122
+ end
123
+
124
+ def test_handshake_response_with_protocol
125
+ res = get("/handshake/build_response_with_protocol")
126
+ assert_includes res.body, "HTTP/1.1 101 Switching Protocols"
127
+ assert_includes res.body, "Upgrade: websocket"
128
+ assert_includes res.body, "Connection: Upgrade"
129
+ assert_includes res.body, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
130
+ assert_includes res.body, "Sec-WebSocket-Protocol: chat"
131
+ end
132
+
133
+ def test_handshake_response_omits_protocol_when_empty
134
+ res = get("/handshake/build_response_no_protocol")
135
+ refute_includes res.body, "Sec-WebSocket-Protocol"
136
+ end
137
+
138
+ def test_handshake_split_csv_trims_whitespace
139
+ res = get("/handshake/split_csv")
140
+ assert_equal "a|b|c", res.body
141
+ end
142
+
143
+ def test_frame_reserved_opcode_predicate
144
+ res = get("/frame/reserved_opcode_rejects")
145
+ assert_equal "yes", res.body
146
+ end
147
+
148
+ def test_frame_control_opcode_predicate
149
+ res = get("/frame/control_opcode_classifies")
150
+ assert_equal "yn", res.body
151
+ end
152
+ end