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,197 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Broadcast: in-process topic broker. v1 stores (topic, fd)
|
|
4
|
+
# pairs; publish writes payload bytes to every matching fd. These
|
|
5
|
+
# tests exercise the registry shape via fake fds (-1 / synthetic
|
|
6
|
+
# ints); real delivery to live sockets gets covered when the WS
|
|
7
|
+
# battery lands and integrates Broadcast end-to-end. The publish()
|
|
8
|
+
# return value is "matched count," not "successful writes" -- bad
|
|
9
|
+
# fds silently fail at sphttp_write_str without affecting the
|
|
10
|
+
# match count.
|
|
11
|
+
class TestBroadcast < TepTest
|
|
12
|
+
app_source <<~RB
|
|
13
|
+
require 'sinatra'
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
res.headers["Content-Type"] = "text/plain"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Reset between cases so test ordering doesn't matter.
|
|
20
|
+
get '/reset' do
|
|
21
|
+
Tep::Broadcast.clear.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
get '/subscribe' do
|
|
25
|
+
topic = params[:topic]
|
|
26
|
+
fd = params[:fd].to_i
|
|
27
|
+
Tep::Broadcast.subscribe(topic, fd).to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
get '/subscribe_ws' do
|
|
31
|
+
topic = params[:topic]
|
|
32
|
+
fd = params[:fd].to_i
|
|
33
|
+
Tep::Broadcast.subscribe_ws(topic, fd).to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
get '/unsubscribe' do
|
|
37
|
+
sub_id = params[:sub_id].to_i
|
|
38
|
+
Tep::Broadcast.unsubscribe(sub_id).to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
get '/unsubscribe_fd' do
|
|
42
|
+
fd = params[:fd].to_i
|
|
43
|
+
Tep::Broadcast.unsubscribe_fd(fd).to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
get '/publish' do
|
|
47
|
+
topic = params[:topic]
|
|
48
|
+
payload = params[:payload]
|
|
49
|
+
Tep::Broadcast.publish(topic, payload).to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
get '/subscriber_count' do
|
|
53
|
+
Tep::Broadcast.subscriber_count.to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
get '/subscribers_for' do
|
|
57
|
+
topic = params[:topic]
|
|
58
|
+
Tep::Broadcast.subscribers_for(topic).to_s
|
|
59
|
+
end
|
|
60
|
+
RB
|
|
61
|
+
|
|
62
|
+
# Helper: reset between tests so state doesn't carry.
|
|
63
|
+
def setup
|
|
64
|
+
super
|
|
65
|
+
get("/reset")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def subscribe(topic, fd)
|
|
69
|
+
get("/subscribe?topic=#{topic}&fd=#{fd}").body.to_i
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def publish(topic, payload)
|
|
73
|
+
get("/publish?topic=#{topic}&payload=#{payload}").body.to_i
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def subscriber_count
|
|
77
|
+
get("/subscriber_count").body.to_i
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def subscribers_for(topic)
|
|
81
|
+
get("/subscribers_for?topic=#{topic}").body.to_i
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ---- empty registry ----
|
|
85
|
+
|
|
86
|
+
def test_publish_to_empty_registry_returns_zero
|
|
87
|
+
assert_equal 0, publish("room:lobby", "hello")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_subscriber_count_starts_at_zero
|
|
91
|
+
assert_equal 0, subscriber_count
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_subscribers_for_unknown_topic_is_zero
|
|
95
|
+
assert_equal 0, subscribers_for("never-subscribed")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ---- subscribe + count ----
|
|
99
|
+
|
|
100
|
+
def test_subscribe_grows_registry
|
|
101
|
+
subscribe("room:lobby", -1)
|
|
102
|
+
assert_equal 1, subscriber_count
|
|
103
|
+
assert_equal 1, subscribers_for("room:lobby")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_multiple_subscribers_same_topic
|
|
107
|
+
subscribe("room:lobby", -1)
|
|
108
|
+
subscribe("room:lobby", -2)
|
|
109
|
+
subscribe("room:lobby", -3)
|
|
110
|
+
assert_equal 3, subscribers_for("room:lobby")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_subscribers_segregated_by_topic
|
|
114
|
+
subscribe("room:lobby", -1)
|
|
115
|
+
subscribe("room:lobby", -2)
|
|
116
|
+
subscribe("room:other", -3)
|
|
117
|
+
assert_equal 2, subscribers_for("room:lobby")
|
|
118
|
+
assert_equal 1, subscribers_for("room:other")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ---- publish matching ----
|
|
122
|
+
|
|
123
|
+
def test_publish_returns_matched_count
|
|
124
|
+
subscribe("room:lobby", -1)
|
|
125
|
+
subscribe("room:lobby", -2)
|
|
126
|
+
subscribe("room:other", -3)
|
|
127
|
+
assert_equal 2, publish("room:lobby", "hi")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_publish_to_unmatched_topic_zero
|
|
131
|
+
subscribe("room:lobby", -1)
|
|
132
|
+
assert_equal 0, publish("never", "hi")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ---- unsubscribe (by sub_id) ----
|
|
136
|
+
|
|
137
|
+
def test_unsubscribe_by_id_drops_one
|
|
138
|
+
sub_id = subscribe("room:lobby", -1)
|
|
139
|
+
subscribe("room:lobby", -2)
|
|
140
|
+
get("/unsubscribe?sub_id=#{sub_id}")
|
|
141
|
+
assert_equal 1, subscribers_for("room:lobby")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ---- unsubscribe_fd (by fd, multi-topic) ----
|
|
145
|
+
|
|
146
|
+
def test_unsubscribe_fd_drops_all_for_fd
|
|
147
|
+
subscribe("room:lobby", -1)
|
|
148
|
+
subscribe("room:other", -1) # same fd, different topic
|
|
149
|
+
subscribe("room:lobby", -2)
|
|
150
|
+
dropped = get("/unsubscribe_fd?fd=-1").body.to_i
|
|
151
|
+
assert_equal 2, dropped
|
|
152
|
+
assert_equal 1, subscribers_for("room:lobby")
|
|
153
|
+
assert_equal 0, subscribers_for("room:other")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_unsubscribe_fd_unknown_zero
|
|
157
|
+
subscribe("room:lobby", -1)
|
|
158
|
+
dropped = get("/unsubscribe_fd?fd=-999").body.to_i
|
|
159
|
+
assert_equal 0, dropped
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# ---- subscribe_ws (WebSocket frame mode) ----
|
|
163
|
+
|
|
164
|
+
def test_subscribe_ws_grows_registry
|
|
165
|
+
get("/subscribe_ws?topic=room:lobby&fd=-1")
|
|
166
|
+
assert_equal 1, subscriber_count
|
|
167
|
+
assert_equal 1, subscribers_for("room:lobby")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_subscribe_ws_publish_match_count
|
|
171
|
+
# Subscribe two WS, one raw -- all three should match a publish
|
|
172
|
+
# to that topic (delivery mode doesn't affect match counting).
|
|
173
|
+
get("/subscribe_ws?topic=room:lobby&fd=-1")
|
|
174
|
+
get("/subscribe_ws?topic=room:lobby&fd=-2")
|
|
175
|
+
get("/subscribe?topic=room:lobby&fd=-3")
|
|
176
|
+
assert_equal 3, publish("room:lobby", "hi")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_subscribe_ws_unsubscribe_fd_drops
|
|
180
|
+
# Mixed-mode subscriptions for one fd: subscribe_ws on a
|
|
181
|
+
# different topic + subscribe on the same fd. unsubscribe_fd
|
|
182
|
+
# drops both.
|
|
183
|
+
get("/subscribe_ws?topic=room:lobby&fd=-1")
|
|
184
|
+
get("/subscribe?topic=room:other&fd=-1")
|
|
185
|
+
dropped = get("/unsubscribe_fd?fd=-1").body.to_i
|
|
186
|
+
assert_equal 2, dropped
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ---- clear ----
|
|
190
|
+
|
|
191
|
+
def test_clear_drops_everything
|
|
192
|
+
subscribe("room:lobby", -1)
|
|
193
|
+
subscribe("room:other", -2)
|
|
194
|
+
get("/reset")
|
|
195
|
+
assert_equal 0, subscriber_count
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Broadcast PG backend: cross-worker pub/sub via
|
|
4
|
+
# LISTEN/NOTIFY. Gated on PG_TEST_URL like test_pg.rb.
|
|
5
|
+
#
|
|
6
|
+
# PG_TEST_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres \
|
|
7
|
+
# ruby test/test_broadcast_pg.rb
|
|
8
|
+
#
|
|
9
|
+
# Test strategy: configure the backend at on_start, then exercise
|
|
10
|
+
# publish() + poll_pg_once() within a single tep app instance. PG's
|
|
11
|
+
# LISTEN/NOTIFY delivers a worker's own NOTIFYs back to it (the
|
|
12
|
+
# "LISTEN sees own publishes" property), so a single-process app
|
|
13
|
+
# can validate the full wire round-trip without needing to spin up
|
|
14
|
+
# a second worker.
|
|
15
|
+
class TestBroadcastPg < TepTest
|
|
16
|
+
PG_URL = ENV["PG_TEST_URL"]
|
|
17
|
+
CHANNEL = "tep_broadcast_test_#{$$}"
|
|
18
|
+
|
|
19
|
+
app_source <<~RB
|
|
20
|
+
require 'sinatra'
|
|
21
|
+
|
|
22
|
+
PG_URL = "#{PG_URL}"
|
|
23
|
+
CHANNEL = "#{CHANNEL}"
|
|
24
|
+
|
|
25
|
+
on_start do
|
|
26
|
+
Tep::Broadcast.enable_pg_backend(PG_URL, CHANNEL)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
res.headers["Content-Type"] = "text/plain"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
get '/reset' do
|
|
34
|
+
Tep::Broadcast.clear.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
get '/subscribe' do
|
|
38
|
+
topic = params[:topic]
|
|
39
|
+
fd = params[:fd].to_i
|
|
40
|
+
Tep::Broadcast.subscribe(topic, fd).to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
get '/publish' do
|
|
44
|
+
topic = params[:topic]
|
|
45
|
+
payload = params[:payload]
|
|
46
|
+
Tep::Broadcast.publish(topic, payload).to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
get '/poll' do
|
|
50
|
+
timeout = params[:timeout].to_i
|
|
51
|
+
Tep::Broadcast.poll_pg_once(timeout).to_s
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
get '/encode_wire' do
|
|
55
|
+
topic = params[:topic]
|
|
56
|
+
payload = params[:payload]
|
|
57
|
+
Tep::Broadcast.encode_wire(topic, payload)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
get '/decode_wire' do
|
|
61
|
+
wire = params[:wire]
|
|
62
|
+
Tep::Broadcast.deliver_wire_local(wire).to_s
|
|
63
|
+
end
|
|
64
|
+
RB
|
|
65
|
+
|
|
66
|
+
def setup
|
|
67
|
+
if PG_URL.nil? || PG_URL.empty?
|
|
68
|
+
skip "PG_TEST_URL not set (e.g. PG_TEST_URL=postgresql:///postgres). " \
|
|
69
|
+
"See test/test_pg.rb header for the docker recipe."
|
|
70
|
+
end
|
|
71
|
+
super
|
|
72
|
+
get("/reset")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ---- Wire format round-trip (no PG, no NOTIFY -- pure encoding) ----
|
|
76
|
+
|
|
77
|
+
def test_encode_wire_length_prefixed
|
|
78
|
+
res = get("/encode_wire?topic=room:lobby&payload=hello")
|
|
79
|
+
# "10:room:lobbyhello" -- 10 chars in topic "room:lobby" then payload "hello"
|
|
80
|
+
assert_equal "10:room:lobbyhello", res.body
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_encode_wire_empty_payload
|
|
84
|
+
res = get("/encode_wire?topic=t&payload=")
|
|
85
|
+
assert_equal "1:t", res.body
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_decode_wire_delivers_to_local_subs
|
|
89
|
+
# Subscribe a fake fd to a topic, then decode-and-deliver a
|
|
90
|
+
# wire-format payload as if it had come in via PG NOTIFY.
|
|
91
|
+
get("/subscribe?topic=room:lobby&fd=-1")
|
|
92
|
+
res = get("/decode_wire?wire=10:room:lobbyhello")
|
|
93
|
+
# Matched 1 local subscriber.
|
|
94
|
+
assert_equal "1", res.body
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_decode_wire_unsubscribed_topic_zero
|
|
98
|
+
res = get("/decode_wire?wire=4:nope")
|
|
99
|
+
assert_equal "0", res.body
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ---- End-to-end PG NOTIFY round trip ----
|
|
103
|
+
|
|
104
|
+
def test_publish_then_poll_round_trips_via_pg
|
|
105
|
+
# Publish a message -- NOTIFY's PG.
|
|
106
|
+
get("/publish?topic=pg_round_trip&payload=ping")
|
|
107
|
+
# Poll for the NOTIFY (we sent it; LISTEN sees own publishes).
|
|
108
|
+
res = get("/poll?timeout=2000")
|
|
109
|
+
assert_equal "1", res.body
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_poll_returns_zero_on_timeout
|
|
113
|
+
# With no preceding publish + no other publisher on the channel,
|
|
114
|
+
# poll should time out cleanly.
|
|
115
|
+
res = get("/poll?timeout=100")
|
|
116
|
+
assert_equal "0", res.body
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_publish_with_local_sub_also_matches_local
|
|
120
|
+
# Local fan-out still works alongside PG NOTIFY. Subscribe a
|
|
121
|
+
# local fake fd, publish -- match count reflects the local sub.
|
|
122
|
+
get("/subscribe?topic=mixed_topic&fd=-1")
|
|
123
|
+
res = get("/publish?topic=mixed_topic&payload=hi")
|
|
124
|
+
assert_equal "1", res.body
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_publish_with_no_local_sub_matches_zero_but_still_notifies
|
|
128
|
+
# No local subs -- match count is 0, but publish still ran the
|
|
129
|
+
# PG NOTIFY (subsequent poll confirms).
|
|
130
|
+
res = get("/publish?topic=remote_only&payload=hi")
|
|
131
|
+
assert_equal "0", res.body
|
|
132
|
+
poll_res = get("/poll?timeout=2000")
|
|
133
|
+
assert_equal "1", poll_res.body
|
|
134
|
+
end
|
|
135
|
+
end
|
data/test/test_cache.rb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# HTTP caching battery (issue #152): Cache-Control / ETag / Last-Modified
|
|
4
|
+
# response helpers + conditional-GET 304 short-circuit.
|
|
5
|
+
class TestCache < TepTest
|
|
6
|
+
app_source <<~RB
|
|
7
|
+
require 'sinatra'
|
|
8
|
+
|
|
9
|
+
get '/etag' do
|
|
10
|
+
res.etag("v1")
|
|
11
|
+
"etag-body"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
get '/lastmod' do
|
|
15
|
+
res.last_modified(1700000000) # fixed epoch -> stable Last-Modified
|
|
16
|
+
"lastmod-body"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
get '/cc' do
|
|
20
|
+
res.cache_control("public, max-age=60")
|
|
21
|
+
"cc-body"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
get '/exp' do
|
|
25
|
+
res.expires(60)
|
|
26
|
+
"exp-body"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
get '/nostore' do
|
|
30
|
+
res.no_store
|
|
31
|
+
"ns-body"
|
|
32
|
+
end
|
|
33
|
+
RB
|
|
34
|
+
|
|
35
|
+
# ---- ETag / If-None-Match ----
|
|
36
|
+
|
|
37
|
+
def test_etag_present_on_normal_get
|
|
38
|
+
res = get("/etag")
|
|
39
|
+
assert_equal "200", res.code
|
|
40
|
+
assert_equal "\"v1\"", res["ETag"]
|
|
41
|
+
assert_equal "etag-body", res.body
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_if_none_match_match_returns_304_no_body
|
|
45
|
+
res = get("/etag", {"If-None-Match" => "\"v1\""})
|
|
46
|
+
assert_equal "304", res.code
|
|
47
|
+
assert_equal "", res.body.to_s
|
|
48
|
+
assert_equal "\"v1\"", res["ETag"] # validator preserved on 304
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_if_none_match_star_returns_304
|
|
52
|
+
res = get("/etag", {"If-None-Match" => "*"})
|
|
53
|
+
assert_equal "304", res.code
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_if_none_match_mismatch_returns_200
|
|
57
|
+
res = get("/etag", {"If-None-Match" => "\"other\""})
|
|
58
|
+
assert_equal "200", res.code
|
|
59
|
+
assert_equal "etag-body", res.body
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ---- Last-Modified / If-Modified-Since ----
|
|
63
|
+
|
|
64
|
+
def test_last_modified_header_is_http_date
|
|
65
|
+
res = get("/lastmod")
|
|
66
|
+
assert_equal "200", res.code
|
|
67
|
+
assert_match(/GMT\z/, res["Last-Modified"])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_if_modified_since_equal_returns_304
|
|
71
|
+
first = get("/lastmod")
|
|
72
|
+
res = get("/lastmod", {"If-Modified-Since" => first["Last-Modified"]})
|
|
73
|
+
assert_equal "304", res.code
|
|
74
|
+
assert_equal "", res.body.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_if_modified_since_older_returns_200
|
|
78
|
+
res = get("/lastmod", {"If-Modified-Since" => "Sat, 01 Jan 2000 00:00:00 GMT"})
|
|
79
|
+
assert_equal "200", res.code
|
|
80
|
+
assert_equal "lastmod-body", res.body
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# ---- Cache-Control / Expires ----
|
|
84
|
+
|
|
85
|
+
def test_cache_control_verbatim
|
|
86
|
+
assert_equal "public, max-age=60", get("/cc")["Cache-Control"]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_expires_sets_expires_and_max_age
|
|
90
|
+
res = get("/exp")
|
|
91
|
+
assert_equal "max-age=60", res["Cache-Control"]
|
|
92
|
+
assert_match(/GMT\z/, res["Expires"])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_no_store_shortcut
|
|
96
|
+
assert_equal "no-store", get("/nostore")["Cache-Control"]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Caching phase 2 (#152): static files served via send_file (public_dir)
|
|
4
|
+
# carry a size-mtime ETag + Last-Modified and revalidate to 304.
|
|
5
|
+
class TestCacheStatic < TepTest
|
|
6
|
+
app_source <<~RB
|
|
7
|
+
set :public_dir, '#{File.expand_path("../public", __dir__)}'
|
|
8
|
+
|
|
9
|
+
get '/' do
|
|
10
|
+
"root"
|
|
11
|
+
end
|
|
12
|
+
RB
|
|
13
|
+
|
|
14
|
+
def test_static_file_has_validators
|
|
15
|
+
res = get("/hello.txt")
|
|
16
|
+
assert_equal "200", res.code
|
|
17
|
+
refute_nil res["ETag"]
|
|
18
|
+
assert_match(/GMT\z/, res["Last-Modified"])
|
|
19
|
+
assert_match(/static file serving/, res.body)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_static_if_none_match_returns_304
|
|
23
|
+
etag = get("/hello.txt")["ETag"]
|
|
24
|
+
res = get("/hello.txt", {"If-None-Match" => etag})
|
|
25
|
+
assert_equal "304", res.code
|
|
26
|
+
assert_equal "", res.body.to_s
|
|
27
|
+
assert_equal etag, res["ETag"] # validator preserved on 304
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_static_if_modified_since_equal_returns_304
|
|
31
|
+
lm = get("/hello.txt")["Last-Modified"]
|
|
32
|
+
res = get("/hello.txt", {"If-Modified-Since" => lm})
|
|
33
|
+
assert_equal "304", res.code
|
|
34
|
+
assert_equal "", res.body.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_static_if_modified_since_old_returns_200
|
|
38
|
+
res = get("/hello.txt", {"If-Modified-Since" => "Sat, 01 Jan 2000 00:00:00 GMT"})
|
|
39
|
+
assert_equal "200", res.code
|
|
40
|
+
assert_match(/static file serving/, res.body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_static_if_none_match_mismatch_returns_200
|
|
44
|
+
res = get("/hello.txt", {"If-None-Match" => "\"nope\""})
|
|
45
|
+
assert_equal "200", res.code
|
|
46
|
+
assert_match(/static file serving/, res.body)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestCookies < TepTest
|
|
4
|
+
app_source <<~RB
|
|
5
|
+
get '/echo' do
|
|
6
|
+
"name=" + cookies["name"] + " mood=" + cookies["mood"]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
get '/set' do
|
|
10
|
+
set_cookie "user", "alice"
|
|
11
|
+
"ok"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
get '/set-flagged' do
|
|
15
|
+
set_cookie "session_id", "xyz"
|
|
16
|
+
"ok"
|
|
17
|
+
end
|
|
18
|
+
RB
|
|
19
|
+
|
|
20
|
+
def test_round_trip
|
|
21
|
+
res = get("/echo", "Cookie" => "name=alice; mood=happy")
|
|
22
|
+
assert_equal "200", res.code
|
|
23
|
+
assert_equal "name=alice mood=happy", res.body
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_url_decoded_value
|
|
27
|
+
res = get("/echo", "Cookie" => "name=hello%20world; mood=ok")
|
|
28
|
+
assert_equal "name=hello world mood=ok", res.body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_missing_cookie_is_empty
|
|
32
|
+
res = get("/echo", "Cookie" => "name=alice")
|
|
33
|
+
assert_equal "name=alice mood=", res.body
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_set_cookie_writes_header
|
|
37
|
+
res = get("/set")
|
|
38
|
+
assert_equal "200", res.code
|
|
39
|
+
assert_equal "user=alice", res["set-cookie"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_set_cookie_value_is_url_encoded
|
|
43
|
+
res = get("/set-flagged")
|
|
44
|
+
assert_match(/^session_id=xyz/, res["set-cookie"])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_no_cookie_header_no_crash
|
|
48
|
+
res = get("/echo")
|
|
49
|
+
assert_equal "200", res.code
|
|
50
|
+
assert_equal "name= mood=", res.body
|
|
51
|
+
end
|
|
52
|
+
end
|
data/test/test_erb.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestErb < TepTest
|
|
4
|
+
app_source <<~RB
|
|
5
|
+
require 'sinatra'
|
|
6
|
+
|
|
7
|
+
set :views, '#{File.expand_path("views", __dir__)}'
|
|
8
|
+
|
|
9
|
+
get '/hello/:who' do
|
|
10
|
+
erb :hello, locals: { name: params[:who], mood: "happy" }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
get '/sober/:who' do
|
|
14
|
+
erb :hello, locals: { name: params[:who], mood: "neutral" }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
get '/list/:n' do
|
|
18
|
+
erb :list, locals: { count: params[:n] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
get '/no-locals' do
|
|
22
|
+
erb :hello
|
|
23
|
+
end
|
|
24
|
+
RB
|
|
25
|
+
|
|
26
|
+
def test_simple_interpolation
|
|
27
|
+
res = get("/hello/world")
|
|
28
|
+
assert_equal "200", res.code
|
|
29
|
+
assert_match(/hello, world!/, res.body)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_conditional_block
|
|
33
|
+
happy = get("/hello/world")
|
|
34
|
+
assert_match(/cheerful/, happy.body)
|
|
35
|
+
sober = get("/sober/world")
|
|
36
|
+
refute_match(/cheerful/, sober.body)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_loop_block
|
|
40
|
+
res = get("/list/3")
|
|
41
|
+
assert_equal "200", res.code
|
|
42
|
+
assert_match(/<li>item 0</, res.body)
|
|
43
|
+
assert_match(/<li>item 1</, res.body)
|
|
44
|
+
assert_match(/<li>item 2</, res.body)
|
|
45
|
+
refute_match(/<li>item 3</, res.body)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_no_locals_renders_empty_for_missing_keys
|
|
49
|
+
res = get("/no-locals")
|
|
50
|
+
assert_equal "200", res.code
|
|
51
|
+
assert_match(/hello, !/, res.body)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Sinatra-style `@ivar` template locals: a handler (or `before`
|
|
4
|
+
# filter) sets `@name = ...`, and the template reads it via
|
|
5
|
+
# `<%= @name %>`. The translator stores ivars on a per-request bag
|
|
6
|
+
# (req.ivars) and threads it as a second arg to `tep_view_<name>`.
|
|
7
|
+
class TestErbIvars < TepTest
|
|
8
|
+
app_source <<~RB
|
|
9
|
+
require 'sinatra'
|
|
10
|
+
|
|
11
|
+
set :views, '#{File.expand_path("views", __dir__)}'
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
@greeting = "filter-said-hi"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
get '/greet/:who/:n' do
|
|
18
|
+
@name = params[:who]
|
|
19
|
+
@count = params[:n]
|
|
20
|
+
erb :greet
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
get '/mixed/:who' do
|
|
24
|
+
@name = params[:who]
|
|
25
|
+
erb :mixed, locals: { greeting: "from-explicit-locals" }
|
|
26
|
+
end
|
|
27
|
+
RB
|
|
28
|
+
|
|
29
|
+
def test_ivar_threading
|
|
30
|
+
res = get("/greet/alice/1")
|
|
31
|
+
assert_equal "200", res.code
|
|
32
|
+
assert_match(/hi, alice!/, res.body)
|
|
33
|
+
assert_match(/visited 1 times/, res.body)
|
|
34
|
+
assert_match(/welcome\./, res.body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_ivar_int_to_s_coercion
|
|
38
|
+
# @count = params[:n] writes a string already, but the rewriter's
|
|
39
|
+
# `.to_s` wrap means the same code would still work if @count
|
|
40
|
+
# held a literal integer (which the template's <%= ... %> renders
|
|
41
|
+
# back to a string).
|
|
42
|
+
res = get("/greet/bob/3")
|
|
43
|
+
assert_equal "200", res.code
|
|
44
|
+
assert_match(/visited 3 times/, res.body)
|
|
45
|
+
refute_match(/welcome\./, res.body) # only renders for count == "1"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_locals_and_ivars_coexist
|
|
49
|
+
res = get("/mixed/charlie")
|
|
50
|
+
assert_equal "200", res.code
|
|
51
|
+
assert_match(/hello, charlie/, res.body)
|
|
52
|
+
assert_match(/locals greeting: from-explicit-locals/, res.body)
|
|
53
|
+
# The before-filter set @greeting; the explicit `locals: {...}`
|
|
54
|
+
# call doesn't shadow it because they're separate hashes in the
|
|
55
|
+
# template signature.
|
|
56
|
+
assert_match(/ivar greeting: filter-said-hi/, res.body)
|
|
57
|
+
end
|
|
58
|
+
end
|