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
data/test/test_mcp.rb ADDED
@@ -0,0 +1,264 @@
1
+ require_relative "helper"
2
+
3
+ # Tep::MCP -- Battery 5 chunk 5.1. Tool DSL + JSON-RPC dispatcher
4
+ # + HTTP-direct route + llms.txt. The translator-emitted classes
5
+ # are exercised via real HTTP against the fixture app.
6
+ class TestMCP < TepTest
7
+ app_source <<~RB
8
+ require 'sinatra'
9
+
10
+ # Grant capabilities via an X-Test-Cap header for the auth
11
+ # gating tests. No real auth provider needed for the dispatch
12
+ # paths -- we just override req.identity with a synthetic one
13
+ # so req.identity.may?(:admin) returns true on demand.
14
+ before do
15
+ if req.req_headers["x-test-cap-admin"].length > 0
16
+ req.identity = Tep::Identity.new(
17
+ "user:42", nil, [:admin])
18
+ end
19
+ end
20
+
21
+ mcp_tool 'greet', "Say hi to someone" do
22
+ param :name, String, "person to greet"
23
+
24
+ on_call do |name:|
25
+ if name.length == 0
26
+ Tep::MCP.error("name required")
27
+ else
28
+ Tep::MCP.text("hello " + name)
29
+ end
30
+ end
31
+ end
32
+
33
+ mcp_tool 'add', "Add two integers" do
34
+ param :a, Integer, "left operand"
35
+ param :b, Integer, "right operand"
36
+
37
+ on_call do |a:, b:|
38
+ Tep::MCP.text((a + b).to_s)
39
+ end
40
+ end
41
+
42
+ # Capped tool -- requires :admin in the calling identity.
43
+ mcp_tool 'wipe_db', "Drop everything (requires :admin)", caps: [:admin] do
44
+ on_call do
45
+ Tep::MCP.text("wiped")
46
+ end
47
+ end
48
+
49
+ mcp_resource 'server/status', "Current server status" do
50
+ on_read do
51
+ Tep::MCP.resource_text("server/status", "uptime: 42")
52
+ end
53
+ end
54
+
55
+ mcp_resource 'server/version', "Server build version" do
56
+ on_read do
57
+ Tep::MCP.resource_text("server/version", "1.0.0-test")
58
+ end
59
+ end
60
+ RB
61
+
62
+ # ---- HTTP-direct invocation ----
63
+
64
+ def test_http_direct_tool_call_returns_text
65
+ res = post("/tools/greet", "{\"name\":\"alice\"}",
66
+ "Content-Type" => "application/json")
67
+ assert_equal "200", res.code
68
+ assert_equal "hello alice", res.body
69
+ end
70
+
71
+ def test_http_direct_tool_error_returns_400
72
+ res = post("/tools/greet", "{\"name\":\"\"}",
73
+ "Content-Type" => "application/json")
74
+ assert_equal "400", res.code
75
+ assert_equal "name required", res.body
76
+ end
77
+
78
+ def test_http_direct_integer_param_round_trip
79
+ res = post("/tools/add", "{\"a\":2,\"b\":40}",
80
+ "Content-Type" => "application/json")
81
+ assert_equal "200", res.code
82
+ assert_equal "42", res.body
83
+ end
84
+
85
+ # ---- JSON-RPC dispatch over /mcp ----
86
+
87
+ def test_mcp_initialize_returns_server_info
88
+ body = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}"
89
+ res = post("/mcp", body, "Content-Type" => "application/json")
90
+ assert_equal "200", res.code
91
+ assert_includes res.body, "\"jsonrpc\":\"2.0\""
92
+ assert_includes res.body, "\"id\":1"
93
+ assert_includes res.body, "\"serverInfo\""
94
+ assert_includes res.body, "\"protocolVersion\""
95
+ end
96
+
97
+ def test_mcp_tools_list_returns_both_tools
98
+ body = "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}"
99
+ res = post("/mcp", body, "Content-Type" => "application/json")
100
+ assert_equal "200", res.code
101
+ assert_includes res.body, "\"name\":\"greet\""
102
+ assert_includes res.body, "\"name\":\"add\""
103
+ assert_includes res.body, "\"description\":\"Say hi to someone\""
104
+ assert_includes res.body, "\"inputSchema\""
105
+ # Schema should encode integer params as JSON Schema integer type.
106
+ assert_includes res.body, "\"type\":\"integer\""
107
+ end
108
+
109
+ def test_mcp_tools_call_round_trips_text_content
110
+ body = "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\"," +
111
+ "\"params\":{\"name\":\"greet\",\"arguments\":{\"name\":\"bob\"}}}"
112
+ res = post("/mcp", body, "Content-Type" => "application/json")
113
+ assert_equal "200", res.code
114
+ assert_includes res.body, "\"id\":3"
115
+ assert_includes res.body, "\"text\":\"hello bob\""
116
+ assert_includes res.body, "\"isError\":false"
117
+ end
118
+
119
+ def test_mcp_tools_call_propagates_is_error
120
+ body = "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\"," +
121
+ "\"params\":{\"name\":\"greet\",\"arguments\":{\"name\":\"\"}}}"
122
+ res = post("/mcp", body, "Content-Type" => "application/json")
123
+ assert_equal "200", res.code
124
+ assert_includes res.body, "\"text\":\"name required\""
125
+ assert_includes res.body, "\"isError\":true"
126
+ end
127
+
128
+ def test_mcp_tools_call_unknown_tool_returns_error_envelope
129
+ body = "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\"," +
130
+ "\"params\":{\"name\":\"nope\",\"arguments\":{}}}"
131
+ res = post("/mcp", body, "Content-Type" => "application/json")
132
+ assert_equal "200", res.code
133
+ assert_includes res.body, "\"error\""
134
+ assert_includes res.body, "\"code\":-32602"
135
+ assert_includes res.body, "unknown tool"
136
+ end
137
+
138
+ def test_mcp_unknown_method_returns_method_not_found
139
+ body = "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"notreal\"}"
140
+ res = post("/mcp", body, "Content-Type" => "application/json")
141
+ assert_equal "200", res.code
142
+ assert_includes res.body, "\"code\":-32601"
143
+ assert_includes res.body, "method not found"
144
+ end
145
+
146
+ # ---- caps gating (chunk 5.2) ----
147
+
148
+ def test_capped_tool_denies_anonymous_caller
149
+ res = post("/tools/wipe_db", "{}",
150
+ "Content-Type" => "application/json")
151
+ # Anonymous identity has empty caps, so :admin check fails.
152
+ # The tool returns an error Result; HTTP-direct surfaces it
153
+ # as 400 + the error text.
154
+ assert_equal "400", res.code
155
+ assert_includes res.body, "missing capability: admin"
156
+ end
157
+
158
+ def test_capped_tool_allows_caller_with_required_cap
159
+ res = post("/tools/wipe_db", "{}",
160
+ "Content-Type" => "application/json",
161
+ "X-Test-Cap-Admin" => "1")
162
+ assert_equal "200", res.code
163
+ assert_equal "wiped", res.body
164
+ end
165
+
166
+ def test_capped_tool_over_mcp_returns_isError
167
+ # Same denial path through the JSON-RPC envelope: anonymous
168
+ # caller -> wipe_db -> error Result -> isError:true.
169
+ body = "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\"," +
170
+ "\"params\":{\"name\":\"wipe_db\",\"arguments\":{}}}"
171
+ res = post("/mcp", body, "Content-Type" => "application/json")
172
+ assert_equal "200", res.code
173
+ assert_includes res.body, "\"isError\":true"
174
+ assert_includes res.body, "missing capability: admin"
175
+ end
176
+
177
+ # ---- notifications/initialized (chunk 5.2) ----
178
+
179
+ def test_notifications_initialized_returns_204_no_body
180
+ body = "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}"
181
+ res = post("/mcp", body, "Content-Type" => "application/json")
182
+ assert_equal "204", res.code
183
+ assert_equal "", res.body.to_s
184
+ end
185
+
186
+ # ---- mcp_resource (chunk 5.3) ----
187
+
188
+ def test_http_direct_resource_read_returns_text
189
+ res = get("/resources/server/status")
190
+ assert_equal "200", res.code
191
+ assert_includes res["content-type"].to_s, "text/plain"
192
+ assert_equal "uptime: 42", res.body
193
+ end
194
+
195
+ def test_mcp_initialize_advertises_resources_capability
196
+ body = "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"initialize\"}"
197
+ res = post("/mcp", body, "Content-Type" => "application/json")
198
+ assert_equal "200", res.code
199
+ assert_includes res.body, "\"resources\":{}"
200
+ end
201
+
202
+ def test_mcp_resources_list_returns_both_resources
203
+ body = "{\"jsonrpc\":\"2.0\",\"id\":12,\"method\":\"resources/list\"}"
204
+ res = post("/mcp", body, "Content-Type" => "application/json")
205
+ assert_equal "200", res.code
206
+ assert_includes res.body, "\"uri\":\"server/status\""
207
+ assert_includes res.body, "\"uri\":\"server/version\""
208
+ assert_includes res.body, "\"description\":\"Current server status\""
209
+ assert_includes res.body, "\"mimeType\":\"text/plain\""
210
+ end
211
+
212
+ def test_mcp_resources_read_round_trips_content
213
+ body = "{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"resources/read\"," +
214
+ "\"params\":{\"uri\":\"server/status\"}}"
215
+ res = post("/mcp", body, "Content-Type" => "application/json")
216
+ assert_equal "200", res.code
217
+ assert_includes res.body, "\"uri\":\"server/status\""
218
+ assert_includes res.body, "\"mimeType\":\"text/plain\""
219
+ assert_includes res.body, "\"text\":\"uptime: 42\""
220
+ end
221
+
222
+ def test_mcp_resources_read_unknown_uri_errors
223
+ body = "{\"jsonrpc\":\"2.0\",\"id\":14,\"method\":\"resources/read\"," +
224
+ "\"params\":{\"uri\":\"nope\"}}"
225
+ res = post("/mcp", body, "Content-Type" => "application/json")
226
+ assert_equal "200", res.code
227
+ assert_includes res.body, "\"code\":-32602"
228
+ assert_includes res.body, "unknown resource"
229
+ end
230
+
231
+ # ---- openapi.json (chunk 5.4) ----
232
+
233
+ def test_openapi_json_lists_tool_paths
234
+ res = get("/openapi.json")
235
+ assert_equal "200", res.code
236
+ assert_includes res["content-type"].to_s, "application/json"
237
+ assert_includes res.body, "\"openapi\":\"3.0.3\""
238
+ assert_includes res.body, "\"/tools/greet\""
239
+ assert_includes res.body, "\"/tools/add\""
240
+ # Integer params declared as integer in the OpenAPI schema.
241
+ assert_includes res.body, "\"type\":\"integer\""
242
+ end
243
+
244
+ def test_openapi_json_lists_resource_paths
245
+ res = get("/openapi.json")
246
+ assert_includes res.body, "\"/resources/server/status\""
247
+ assert_includes res.body, "\"/resources/server/version\""
248
+ end
249
+
250
+ # ---- llms.txt discovery ----
251
+
252
+ def test_llms_txt_lists_tools_with_descriptions
253
+ res = get("/llms.txt")
254
+ assert_equal "200", res.code
255
+ assert_includes res["content-type"].to_s, "text/markdown"
256
+ assert_includes res.body, "MCP-endpoint: /mcp"
257
+ assert_includes res.body, "OpenAPI: /openapi.json"
258
+ assert_includes res.body, "## Tools"
259
+ assert_includes res.body, "greet -- Say hi to someone"
260
+ assert_includes res.body, "add -- Add two integers"
261
+ assert_includes res.body, "## Resources"
262
+ assert_includes res.body, "server/status -- Current server status"
263
+ end
264
+ end
@@ -0,0 +1,54 @@
1
+ require_relative "helper"
2
+
3
+ # send_file, configure, and __END__ inline templates -- the three small
4
+ # wins added on top of the v0.2 surface.
5
+ class TestMiscV02 < TepTest
6
+ app_source <<~RB
7
+ require 'sinatra'
8
+
9
+ configure do
10
+ $configured = "always"
11
+ end
12
+
13
+ configure :production do
14
+ $configured = "prod"
15
+ end
16
+
17
+ get '/configured' do
18
+ "configured=" + ($configured || "nil").to_s
19
+ end
20
+
21
+ get '/file' do
22
+ send_file 'public/hello.txt'
23
+ end
24
+
25
+ get '/inline' do
26
+ erb :inline_hi, locals: { who: "world" }
27
+ end
28
+
29
+ __END__
30
+
31
+ @@ inline_hi
32
+ <h1>hi <%= locals["who"] %> from inline</h1>
33
+ RB
34
+
35
+ def test_configure_always_runs
36
+ res = get("/configured")
37
+ # Both the bare and :production blocks ran in dev env: "always" set first,
38
+ # then "prod" only if env=production. Default env is development, so the
39
+ # :production block is gated off and the value stays "always".
40
+ assert_equal "configured=always", res.body
41
+ end
42
+
43
+ def test_send_file_streams_static
44
+ res = get("/file")
45
+ assert_equal "200", res.code
46
+ assert_match(/static file serving works/, res.body)
47
+ end
48
+
49
+ def test_inline_template_renders
50
+ res = get("/inline")
51
+ assert_equal "200", res.code
52
+ assert_match(%r{hi world from inline}, res.body)
53
+ end
54
+ end
@@ -0,0 +1,43 @@
1
+ require_relative "helper"
2
+
3
+ class TestModular < TepTest
4
+ app_source <<~RB
5
+ require 'sinatra/base'
6
+
7
+ class Api < Sinatra::Base
8
+ before do
9
+ response.headers["X-App"] = "Api"
10
+ end
11
+
12
+ get '/api/health' do
13
+ "ok"
14
+ end
15
+ end
16
+
17
+ class Admin < Sinatra::Base
18
+ get '/admin/dashboard' do
19
+ "admin"
20
+ end
21
+ end
22
+
23
+ Api.run!
24
+ Admin.run!
25
+ RB
26
+
27
+ def test_first_app_route
28
+ res = get("/api/health")
29
+ assert_equal "200", res.code
30
+ assert_equal "ok", res.body
31
+ end
32
+
33
+ def test_second_app_route
34
+ res = get("/admin/dashboard")
35
+ assert_equal "200", res.code
36
+ assert_equal "admin", res.body
37
+ end
38
+
39
+ def test_modular_before_filter_ran
40
+ res = get("/api/health")
41
+ assert_equal "Api", res["x-app"]
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ require_relative "helper"
2
+
3
+ # Multiple `before do` / `after do` blocks should all run, in order.
4
+ class TestMultiFilters < TepTest
5
+ app_source <<~RB
6
+ require 'sinatra'
7
+
8
+ before do
9
+ response.headers["X-First"] = "1"
10
+ end
11
+
12
+ before do
13
+ response.headers["X-Second"] = "2"
14
+ end
15
+
16
+ after do
17
+ response.headers["X-After-A"] = "a"
18
+ end
19
+
20
+ after do
21
+ response.headers["X-After-B"] = "b"
22
+ end
23
+
24
+ get '/' do
25
+ "ok"
26
+ end
27
+ RB
28
+
29
+ def test_both_before_filters_run
30
+ res = get("/")
31
+ assert_equal "1", res["x-first"]
32
+ assert_equal "2", res["x-second"]
33
+ end
34
+
35
+ def test_both_after_filters_run
36
+ res = get("/")
37
+ assert_equal "a", res["x-after-a"]
38
+ assert_equal "b", res["x-after-b"]
39
+ end
40
+ end
@@ -0,0 +1,57 @@
1
+ require_relative "helper"
2
+
3
+ # Tep's Mustache subset (build-time AOT). Documented surface:
4
+ # `{{var}}` (escaped), `{{{var}}}` / `{{& var}}` (raw),
5
+ # `{{@ivar}}` (escaped or raw via triple-stache), `{{!comment}}`
6
+ # (dropped). Sections / partials / delimiter swaps are deliberately
7
+ # unsupported and the compiler raises at build time if reached.
8
+ class TestMustache < TepTest
9
+ app_source <<~RB
10
+ require 'sinatra'
11
+
12
+ set :views, '#{File.expand_path("views", __dir__)}'
13
+
14
+ get '/m/simple/:who' do
15
+ mustache :m_simple, locals: { name: params[:who], greeting: "hi", snippet: "<b>BOLD</b>" }
16
+ end
17
+
18
+ before do
19
+ @raw = "<i>I</i>"
20
+ end
21
+
22
+ get '/m/ivars/:who/:n' do
23
+ @name = params[:who]
24
+ @count = params[:n]
25
+ mustache :m_ivars
26
+ end
27
+ RB
28
+
29
+ def test_simple_escaped_and_raw
30
+ res = get("/m/simple/alice")
31
+ assert_equal "200", res.code
32
+ assert_match(/hello, alice!/, res.body)
33
+ assert_match(/greeting: hi/, res.body)
34
+ # `{{{snippet}}}` (raw) keeps the live tag.
35
+ assert_match(/<p>raw html: <b>BOLD<\/b><\/p>/, res.body)
36
+ # comment line is dropped
37
+ refute_match(/this comment is dropped/, res.body)
38
+ end
39
+
40
+ def test_html_escape_dangerous_chars
41
+ # `<` and `>` need URL-encoding to even reach the server. The
42
+ # escaped `{{name}}` form must then render `&lt;script&gt;`,
43
+ # not the live tag.
44
+ res = get("/m/simple/%3Cscript%3E")
45
+ assert_match(/hello, &lt;script&gt;!/, res.body)
46
+ refute_match(/hello, <script>/, res.body)
47
+ end
48
+
49
+ def test_ivar_via_at_prefix
50
+ res = get("/m/ivars/bob/4")
51
+ assert_equal "200", res.code
52
+ assert_match(/hi, bob/, res.body)
53
+ assert_match(/visited: 4/, res.body)
54
+ # `{{{@raw}}}` (raw ivar) keeps the `<i>I</i>` literal
55
+ assert_match(/raw ivar: <i>I<\/i>/, res.body)
56
+ end
57
+ end