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
data/test/test_jwt.rb
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::Jwt -- HS256 encode + decode + verify, plus the b64url
|
|
4
|
+
# helpers in sphttp.c that back it.
|
|
5
|
+
class TestJwt < TepTest
|
|
6
|
+
app_source <<~RB
|
|
7
|
+
require 'sinatra'
|
|
8
|
+
|
|
9
|
+
SECRET = "supersecret"
|
|
10
|
+
|
|
11
|
+
post '/issue' do
|
|
12
|
+
res.headers["Content-Type"] = "text/plain"
|
|
13
|
+
user = Tep::Json.get_str(req.raw_body, "user")
|
|
14
|
+
payload = "{" + Tep::Json.encode_pair_str("sub", user) + "}"
|
|
15
|
+
Tep::Jwt.encode_hs256(payload, SECRET)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
post '/verify' do
|
|
19
|
+
res.headers["Content-Type"] = "text/plain"
|
|
20
|
+
Tep::Jwt.verify_hs256(req.raw_body, SECRET) ? "ok" : "bad"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
post '/decode' do
|
|
24
|
+
res.headers["Content-Type"] = "text/plain"
|
|
25
|
+
Tep::Jwt.decode_payload(req.raw_body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
post '/verify_and_decode' do
|
|
29
|
+
res.headers["Content-Type"] = "text/plain"
|
|
30
|
+
Tep::Jwt.verify_and_decode(req.raw_body, SECRET)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
post '/b64u_encode' do
|
|
34
|
+
res.headers["Content-Type"] = "text/plain"
|
|
35
|
+
Crypto.sp_crypto_b64url_encode(req.raw_body)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
post '/b64u_decode' do
|
|
39
|
+
res.headers["Content-Type"] = "text/plain"
|
|
40
|
+
Crypto.sp_crypto_b64url_decode(req.raw_body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
post '/timing_eq' do
|
|
44
|
+
res.headers["Content-Type"] = "text/plain"
|
|
45
|
+
a = Tep::Json.get_str(req.raw_body, "a")
|
|
46
|
+
b = Tep::Json.get_str(req.raw_body, "b")
|
|
47
|
+
Tep::Jwt.timing_safe_eq(a, b) ? "yes" : "no"
|
|
48
|
+
end
|
|
49
|
+
RB
|
|
50
|
+
|
|
51
|
+
def issue(user)
|
|
52
|
+
post("/issue", %({"user":"#{user}"})).body.strip
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_encode_decode_round_trip
|
|
56
|
+
token = issue("alice")
|
|
57
|
+
parts = token.split(".")
|
|
58
|
+
assert_equal 3, parts.length
|
|
59
|
+
# Decode the payload via the route -- exercises sphttp_b64url_decode
|
|
60
|
+
# in the same path the verify uses.
|
|
61
|
+
res = post("/decode", token)
|
|
62
|
+
assert_match(/"sub":"alice"/, res.body)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_verify_signature_match
|
|
66
|
+
token = issue("alice")
|
|
67
|
+
res = post("/verify", token)
|
|
68
|
+
assert_equal "ok", res.body.strip
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_verify_rejects_tampered_signature
|
|
72
|
+
token = issue("alice")
|
|
73
|
+
# Flip a byte in the signature segment.
|
|
74
|
+
bad = token + "x"
|
|
75
|
+
res = post("/verify", bad)
|
|
76
|
+
assert_equal "bad", res.body.strip
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_verify_rejects_tampered_payload
|
|
80
|
+
token = issue("alice")
|
|
81
|
+
# Replace "alice" in the encoded payload with a different
|
|
82
|
+
# subject and re-stitch -- signature should no longer match.
|
|
83
|
+
parts = token.split(".")
|
|
84
|
+
new_payload = '{"sub":"mallory"}'
|
|
85
|
+
require "base64"
|
|
86
|
+
new_b64 = Base64.urlsafe_encode64(new_payload, padding: false)
|
|
87
|
+
forged = parts[0] + "." + new_b64 + "." + parts[2]
|
|
88
|
+
res = post("/verify", forged)
|
|
89
|
+
assert_equal "bad", res.body.strip
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_verify_and_decode_one_shot
|
|
93
|
+
token = issue("alice")
|
|
94
|
+
res = post("/verify_and_decode", token)
|
|
95
|
+
assert_match(/"sub":"alice"/, res.body)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_verify_and_decode_returns_empty_on_bad_sig
|
|
99
|
+
token = issue("alice")
|
|
100
|
+
res = post("/verify_and_decode", token + "x")
|
|
101
|
+
assert_equal "", res.body.strip
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_b64url_encode_round_trip
|
|
105
|
+
plain = "hello, world!"
|
|
106
|
+
enc = post("/b64u_encode", plain).body.strip
|
|
107
|
+
# JWT-style: no padding.
|
|
108
|
+
refute_match(/=/, enc)
|
|
109
|
+
dec = post("/b64u_decode", enc).body.strip
|
|
110
|
+
assert_equal plain, dec
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_b64url_round_trip_with_special_chars
|
|
114
|
+
plain = '{"a":"x?y","z":1}'
|
|
115
|
+
enc = post("/b64u_encode", plain).body.strip
|
|
116
|
+
dec = post("/b64u_decode", enc).body.strip
|
|
117
|
+
assert_equal plain, dec
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_timing_safe_eq
|
|
121
|
+
res = post("/timing_eq", '{"a":"hello","b":"hello"}')
|
|
122
|
+
assert_equal "yes", res.body.strip
|
|
123
|
+
res = post("/timing_eq", '{"a":"hello","b":"world"}')
|
|
124
|
+
assert_equal "no", res.body.strip
|
|
125
|
+
res = post("/timing_eq", '{"a":"hello","b":"hi"}')
|
|
126
|
+
assert_equal "no", res.body.strip
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Interop: the canonical CRuby `jwt` gem must be able to verify
|
|
130
|
+
# tokens we issue. Skipped if the gem isn't installed locally.
|
|
131
|
+
def test_interop_with_jwt_gem
|
|
132
|
+
begin
|
|
133
|
+
require "jwt"
|
|
134
|
+
rescue LoadError
|
|
135
|
+
skip "jwt gem not installed locally"
|
|
136
|
+
end
|
|
137
|
+
token = issue("alice")
|
|
138
|
+
payload, header = JWT.decode(token, "supersecret", true, { algorithm: "HS256" })
|
|
139
|
+
assert_equal "alice", payload["sub"]
|
|
140
|
+
assert_equal "HS256", header["alg"]
|
|
141
|
+
assert_equal "JWT", header["typ"]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::LiveView base class + helpers (Battery 4 chunk 4.1).
|
|
4
|
+
# v1 ships the manual-wiring path: a base class apps subclass +
|
|
5
|
+
# the render_page / dispatch_event cmeths. Auto-wiring lands in
|
|
6
|
+
# 4.2; these tests cover the building blocks.
|
|
7
|
+
class TestLiveView < TepTest
|
|
8
|
+
app_source <<~RB
|
|
9
|
+
require 'sinatra'
|
|
10
|
+
|
|
11
|
+
# A counter view: state is an integer; "inc" increments,
|
|
12
|
+
# "dec" decrements, "reset" zeroes.
|
|
13
|
+
class CounterView < Tep::LiveView
|
|
14
|
+
attr_accessor :count
|
|
15
|
+
def initialize
|
|
16
|
+
super
|
|
17
|
+
@count = 0
|
|
18
|
+
end
|
|
19
|
+
def mount(req)
|
|
20
|
+
# Pull a seed value from the request's params if present;
|
|
21
|
+
# otherwise leave at 0.
|
|
22
|
+
seed = req.params["seed"]
|
|
23
|
+
if seed.length > 0
|
|
24
|
+
@count = seed.to_i
|
|
25
|
+
end
|
|
26
|
+
0
|
|
27
|
+
end
|
|
28
|
+
def render
|
|
29
|
+
"<div id='tep-live-root'>Count: " + @count.to_s + "</div>"
|
|
30
|
+
end
|
|
31
|
+
def handle_event(event, payload, req)
|
|
32
|
+
if event == "inc"
|
|
33
|
+
@count += 1
|
|
34
|
+
elsif event == "dec"
|
|
35
|
+
@count -= 1
|
|
36
|
+
elsif event == "reset"
|
|
37
|
+
@count = 0
|
|
38
|
+
end
|
|
39
|
+
0
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Per-request scratchpad: the test app boots once, but we
|
|
44
|
+
# need a fresh view per request so tests don't pollute each
|
|
45
|
+
# other. The handler routes below construct a view, run the
|
|
46
|
+
# operation, return the result; no cross-request state.
|
|
47
|
+
|
|
48
|
+
before do
|
|
49
|
+
res.headers["Content-Type"] = "text/plain"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
get '/initial_render' do
|
|
53
|
+
v = CounterView.new
|
|
54
|
+
v.mount(req)
|
|
55
|
+
v.render
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
get '/render_page' do
|
|
59
|
+
Tep::LiveView.render_page("<p>hi</p>", "/_live")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
get '/event_inc' do
|
|
63
|
+
v = CounterView.new
|
|
64
|
+
v.mount(req)
|
|
65
|
+
v.dispatch_event_json("{\\"event\\":\\"inc\\",\\"payload\\":\\"\\"}", req)
|
|
66
|
+
v.render
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
get '/event_chain' do
|
|
70
|
+
# Multiple events through the same view to verify state
|
|
71
|
+
# carries forward.
|
|
72
|
+
v = CounterView.new
|
|
73
|
+
v.mount(req)
|
|
74
|
+
v.dispatch_event_json("{\\"event\\":\\"inc\\",\\"payload\\":\\"\\"}", req)
|
|
75
|
+
v.dispatch_event_json("{\\"event\\":\\"inc\\",\\"payload\\":\\"\\"}", req)
|
|
76
|
+
v.dispatch_event_json("{\\"event\\":\\"inc\\",\\"payload\\":\\"\\"}", req)
|
|
77
|
+
v.dispatch_event_json("{\\"event\\":\\"dec\\",\\"payload\\":\\"\\"}", req)
|
|
78
|
+
v.render
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
get '/event_unknown' do
|
|
82
|
+
v = CounterView.new
|
|
83
|
+
v.mount(req)
|
|
84
|
+
v.dispatch_event_json("{\\"event\\":\\"never\\",\\"payload\\":\\"\\"}", req)
|
|
85
|
+
v.render
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
get '/base_class_render' do
|
|
89
|
+
# The Tep::LiveView base class's default render is a noop
|
|
90
|
+
# shell -- subclasses are expected to override.
|
|
91
|
+
Tep::LiveView.new.render
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ---- chunk 4.2: broadcast binding ----
|
|
95
|
+
|
|
96
|
+
# A view bound to a topic. Setting the topic via a class
|
|
97
|
+
# constant rather than a per-instance ivar so the test
|
|
98
|
+
# endpoint doesn't need to thread state across calls.
|
|
99
|
+
class RoomView < Tep::LiveView
|
|
100
|
+
def topic
|
|
101
|
+
"room:lobby"
|
|
102
|
+
end
|
|
103
|
+
def render
|
|
104
|
+
"<div id='tep-live-root'>room:lobby</div>"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Default base class topic.
|
|
109
|
+
get '/base_topic' do
|
|
110
|
+
Tep::LiveView.new.topic
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Subclass with overridden topic.
|
|
114
|
+
get '/room_topic' do
|
|
115
|
+
RoomView.new.topic
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# broadcast_render on a topic-less view is a no-op (returns 0).
|
|
119
|
+
get '/broadcast_noop_topicless' do
|
|
120
|
+
Tep::LiveView.new.broadcast_render.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# ---- Tep.live auto-wiring ----
|
|
124
|
+
#
|
|
125
|
+
# `Tep.live "/auto", CounterView` is lowered by the translator
|
|
126
|
+
# into a GET handler at /auto (initial render + bootstrap JS)
|
|
127
|
+
# and a WS handler at /auto/ws (event dispatch + re-render).
|
|
128
|
+
# The blocking server returns 501 for the WS upgrade, so the
|
|
129
|
+
# test exercises the GET side only.
|
|
130
|
+
Tep.live "/auto", CounterView
|
|
131
|
+
|
|
132
|
+
# ---- chunk 4.3: presence diff binding ----
|
|
133
|
+
|
|
134
|
+
# A view that records every presence diff it receives -- the
|
|
135
|
+
# subclass override of handle_presence_diff pulls a field out
|
|
136
|
+
# and appends to a class-level Array so the test endpoint can
|
|
137
|
+
# report it back.
|
|
138
|
+
class PresenceTrackingView < Tep::LiveView
|
|
139
|
+
def initialize
|
|
140
|
+
super
|
|
141
|
+
@last_principal = ""
|
|
142
|
+
@last_kind = ""
|
|
143
|
+
@last_state = ""
|
|
144
|
+
end
|
|
145
|
+
attr_reader :last_principal, :last_kind, :last_state
|
|
146
|
+
def topic
|
|
147
|
+
"room:lobby"
|
|
148
|
+
end
|
|
149
|
+
def render
|
|
150
|
+
"<div id='tep-live-root'>" + @last_principal + ":" + @last_state + "</div>"
|
|
151
|
+
end
|
|
152
|
+
def handle_presence_diff(diff_json)
|
|
153
|
+
@last_principal = Tep::Json.get_str(diff_json, "principal")
|
|
154
|
+
@last_kind = Tep::Json.get_str(diff_json, "kind")
|
|
155
|
+
@last_state = Tep::Json.get_str(diff_json, "state")
|
|
156
|
+
0
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
get '/presence_diff_default_noop' do
|
|
161
|
+
Tep::LiveView.new.handle_presence_diff("{\\"kind\\":\\"join\\"}").to_s
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
get '/presence_diff_apply' do
|
|
165
|
+
v = PresenceTrackingView.new
|
|
166
|
+
# Feed a synthetic diff JSON.
|
|
167
|
+
diff = "{\\"kind\\":\\"status\\",\\"principal\\":\\"user:42\\"," +
|
|
168
|
+
"\\"state\\":\\"busy\\",\\"note\\":\\"\\"}"
|
|
169
|
+
v.apply_presence_diff_json(diff)
|
|
170
|
+
v.last_principal + "|" + v.last_kind + "|" + v.last_state
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
get '/presence_diff_render_after_apply' do
|
|
174
|
+
v = PresenceTrackingView.new
|
|
175
|
+
diff = "{\\"kind\\":\\"join\\",\\"principal\\":\\"user:99\\"," +
|
|
176
|
+
"\\"state\\":\\"available\\",\\"note\\":\\"\\"}"
|
|
177
|
+
v.apply_presence_diff_json(diff)
|
|
178
|
+
v.render
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# broadcast_render with a topic + an existing subscriber.
|
|
182
|
+
get '/broadcast_render_match_count' do
|
|
183
|
+
# Subscribe a fake fd to room:lobby so broadcast_render has
|
|
184
|
+
# someone to match.
|
|
185
|
+
Tep::Broadcast.clear
|
|
186
|
+
Tep::Broadcast.subscribe("room:lobby", -1)
|
|
187
|
+
v = RoomView.new
|
|
188
|
+
"topic=" + v.topic +
|
|
189
|
+
"|subs=" + Tep::Broadcast.subscribers_for("room:lobby").to_s +
|
|
190
|
+
"|direct_publish=" + Tep::Broadcast.publish("room:lobby", "x").to_s +
|
|
191
|
+
"|broadcast_render=" + v.broadcast_render.to_s
|
|
192
|
+
end
|
|
193
|
+
RB
|
|
194
|
+
|
|
195
|
+
def test_initial_render_default_count
|
|
196
|
+
res = get("/initial_render")
|
|
197
|
+
assert_equal "<div id='tep-live-root'>Count: 0</div>", res.body
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_mount_picks_seed_from_params
|
|
201
|
+
res = get("/initial_render?seed=42")
|
|
202
|
+
assert_equal "<div id='tep-live-root'>Count: 42</div>", res.body
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def test_render_page_wraps_content_and_includes_bootstrap
|
|
206
|
+
res = get("/render_page").body
|
|
207
|
+
assert_includes res, "<!doctype html>"
|
|
208
|
+
assert_includes res, "<p>hi</p>"
|
|
209
|
+
# The bootstrap script connects to the supplied WS path.
|
|
210
|
+
assert_includes res, "new WebSocket"
|
|
211
|
+
assert_includes res, "/_live"
|
|
212
|
+
# Click->event dispatch wire shape (uses t.dataset.event on the
|
|
213
|
+
# client side).
|
|
214
|
+
assert_includes res, "dataset.event"
|
|
215
|
+
# innerHTML/outerHTML swap on incoming frame.
|
|
216
|
+
assert_includes res, "outerHTML"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def test_dispatch_event_inc
|
|
220
|
+
res = get("/event_inc").body
|
|
221
|
+
assert_equal "<div id='tep-live-root'>Count: 1</div>", res
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_dispatch_event_chain_preserves_state_across_events
|
|
225
|
+
# 3 inc + 1 dec = 2
|
|
226
|
+
res = get("/event_chain").body
|
|
227
|
+
assert_equal "<div id='tep-live-root'>Count: 2</div>", res
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_dispatch_event_unknown_event_is_noop
|
|
231
|
+
res = get("/event_unknown").body
|
|
232
|
+
assert_equal "<div id='tep-live-root'>Count: 0</div>", res
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def test_base_class_render_is_empty_shell
|
|
236
|
+
res = get("/base_class_render").body
|
|
237
|
+
assert_equal "<div id='tep-live-root'></div>", res
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# ---- chunk 4.2: broadcast binding ----
|
|
241
|
+
|
|
242
|
+
def test_base_class_topic_is_empty
|
|
243
|
+
assert_equal "", get("/base_topic").body
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def test_subclass_topic_override
|
|
247
|
+
assert_equal "room:lobby", get("/room_topic").body
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_broadcast_render_noop_when_topicless
|
|
251
|
+
# broadcast_render on a view with no topic is a no-op
|
|
252
|
+
# (subscribers can't bind to "" topics).
|
|
253
|
+
assert_equal "0", get("/broadcast_noop_topicless").body
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def test_broadcast_render_publishes_to_topic
|
|
257
|
+
# Pre-subscribe a fake fd to room:lobby. broadcast_render
|
|
258
|
+
# should publish + match the subscriber.
|
|
259
|
+
res = get("/broadcast_render_match_count").body
|
|
260
|
+
# res shape:
|
|
261
|
+
# topic=room:lobby|subs=1|direct_publish=1|broadcast_render=1
|
|
262
|
+
assert_match(/topic=room:lobby/, res)
|
|
263
|
+
assert_match(/subs=1/, res)
|
|
264
|
+
assert_match(/direct_publish=1/, res)
|
|
265
|
+
assert_match(/broadcast_render=1/, res)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# ---- chunk 4.3: presence diff binding ----
|
|
269
|
+
|
|
270
|
+
def test_base_class_handle_presence_diff_is_noop
|
|
271
|
+
# Default returns 0 and doesn't crash on arbitrary JSON.
|
|
272
|
+
assert_equal "0", get("/presence_diff_default_noop").body
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_subclass_handle_presence_diff_receives_diff_fields
|
|
276
|
+
# The subclass override pulls principal/kind/state out of the
|
|
277
|
+
# diff JSON and updates ivars. apply_presence_diff_json is
|
|
278
|
+
# the imeth that bridges JSON to handle_presence_diff.
|
|
279
|
+
assert_equal "user:42|status|busy", get("/presence_diff_apply").body
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_handle_presence_diff_can_drive_render
|
|
283
|
+
# After applying a join diff, render reflects the new state.
|
|
284
|
+
res = get("/presence_diff_render_after_apply").body
|
|
285
|
+
assert_equal "<div id='tep-live-root'>user:99:available</div>", res
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# ---- Tep.live auto-wiring ----
|
|
289
|
+
|
|
290
|
+
def test_tep_live_get_returns_initial_render_wrapped_in_page
|
|
291
|
+
# GET /auto runs the translator-emitted route: instantiate
|
|
292
|
+
# CounterView, mount(req), render, wrap in render_page targeted
|
|
293
|
+
# at /auto/ws.
|
|
294
|
+
res = get("/auto")
|
|
295
|
+
assert_equal "200", res.code
|
|
296
|
+
body = res.body
|
|
297
|
+
# Initial render comes from CounterView#render with @count = 0.
|
|
298
|
+
assert_includes body, "<div id='tep-live-root'>Count: 0</div>"
|
|
299
|
+
# render_page bootstrap JS targets the auto-generated WS path.
|
|
300
|
+
assert_includes body, "/auto/ws"
|
|
301
|
+
# render_page wraps in a full HTML doc with the bootstrap shell.
|
|
302
|
+
assert_includes body, "<!doctype html>"
|
|
303
|
+
assert_includes body, "var ws=new WebSocket("
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_tep_live_get_honors_view_mount
|
|
307
|
+
# CounterView#mount reads ?seed= from req.params and seeds @count.
|
|
308
|
+
res = get("/auto?seed=42")
|
|
309
|
+
assert_includes res.body, "<div id='tep-live-root'>Count: 42</div>"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_tep_live_ws_path_returns_501_under_blocking_server
|
|
313
|
+
# The auto-wired WS path requires the scheduled server. The
|
|
314
|
+
# blocking server returns 501 for WS upgrade attempts (same
|
|
315
|
+
# behavior as a hand-written `websocket` block).
|
|
316
|
+
res = req(:get, "/auto/ws", nil, {
|
|
317
|
+
"Upgrade" => "websocket",
|
|
318
|
+
"Connection" => "Upgrade",
|
|
319
|
+
"Sec-WebSocket-Key" => "x3JJHMbDL1EzLkh9GBhXDw==",
|
|
320
|
+
"Sec-WebSocket-Version" => "13",
|
|
321
|
+
})
|
|
322
|
+
assert_equal "501", res.code
|
|
323
|
+
end
|
|
324
|
+
end
|