host: blog on the editor's sx_content model + render-to-html, 171/171
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 17s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 17s
Pivot blog to the SX editor's content model. The editor (blog/sx/editor.sx)
emits sx_content = SX element markup, NOT content-on-sx CtDoc blocks. So a post
is now a {slug,title,sx_content,status} record in the durable persist KV, and a
post page is render-to-html(parse sx_content) — server-side, static, no client
runtime needed to view.
Endpoints: GET / (HTML index), /<slug>/ (rendered post), /posts (JSON list),
/new (create form); POST /new (form-urlencoded editor ingest, slug from title,
303 redirect), POST /posts (JSON create), PUT/DELETE /posts/<slug>. Writes
behind auth+ACL (edit/blog). Dropped the content-on-sx/Smalltalk preload chain;
added spec/render + web/adapter-html (render-to-html) + lib/dream/form.
BONUS: render-to-html is ~0ms (vs the 2s content-on-sx Smalltalk asHTML) — it
doesn't hit the JIT-miscompiled path, so blog rendering is no longer slow.
Live: blog.rose-ash.com/ lists posts, /welcome/ renders instantly. Reads live;
the form-ingest write path needs an auth decision before going live (browser
forms can't send bearer; needs session or a Caddy basicauth gate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,185 +1,131 @@
|
||||
;; lib/host/tests/blog.sx — the blog published-post read endpoint. A registered
|
||||
;; post renders to HTML at GET /<slug>/; unknown slugs 404. Also pins route
|
||||
;; precedence: the catch-all :slug must NOT shadow domain routes mounted before it.
|
||||
;; lib/host/tests/blog.sx — blog on the editor's content model. Posts are
|
||||
;; {slug,title,sx_content,status} records in the durable KV; a post page is
|
||||
;; render-to-html(parse sx_content). Covers read/render, home index, JSON list,
|
||||
;; slugify, the form-urlencoded editor ingest, and JSON CRUD (auth+ACL guarded).
|
||||
|
||||
(define host-bl-pass 0)
|
||||
(define host-bl-fail 0)
|
||||
(define host-bl-fails (list))
|
||||
|
||||
(define
|
||||
host-bl-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(fn (name actual expected)
|
||||
(if (= actual expected)
|
||||
(set! host-bl-pass (+ host-bl-pass 1))
|
||||
(begin
|
||||
(set! host-bl-fail (+ host-bl-fail 1))
|
||||
(append! host-bl-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define host-bl-req (fn (target) (dream-request "GET" target {} "")))
|
||||
;; feed mounted BEFORE blog so /feed is not captured by the :slug catch-all.
|
||||
(define host-bl-app
|
||||
(host/make-app (list host/feed-routes host/blog-routes)))
|
||||
(define host-bl-app (host/make-app (list host/feed-routes host/blog-routes)))
|
||||
|
||||
;; ── publish a post to a fresh in-memory store (hermetic) ────────────
|
||||
;; ── slugify ─────────────────────────────────────────────────────────
|
||||
(host-bl-test "slugify" (host/blog-slugify "Hello World") "hello-world")
|
||||
(host-bl-test "slugify trims spaces" (host/blog-slugify " A B ") "a-b")
|
||||
|
||||
;; ── render a stored post ────────────────────────────────────────────
|
||||
(host/blog-use-store! (persist/open))
|
||||
(host/blog-publish! "welcome" "Hello SX" "Served by lib/host." 1)
|
||||
(host/blog-put! "hello" "Hello World"
|
||||
"(article (h1 \"Hello World\") (p \"A \" (strong \"bold\") \" word.\"))" "published")
|
||||
|
||||
(host-bl-test
|
||||
"post 200"
|
||||
(dream-status (host-bl-app (host-bl-req "/welcome/")))
|
||||
200)
|
||||
(host-bl-test
|
||||
"post content-type html"
|
||||
(contains? (dream-resp-header (host-bl-app (host-bl-req "/welcome/")) "content-type") "text/html")
|
||||
(host-bl-test "post 200" (dream-status (host-bl-app (host-bl-req "/hello/"))) 200)
|
||||
(host-bl-test "post content-type html"
|
||||
(contains? (dream-resp-header (host-bl-app (host-bl-req "/hello/")) "content-type") "text/html")
|
||||
true)
|
||||
(host-bl-test
|
||||
"post renders heading"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/welcome/"))) "<h1>Hello SX</h1>")
|
||||
(host-bl-test "post renders sx_content markup"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/hello/"))) "<strong>bold</strong>")
|
||||
true)
|
||||
(host-bl-test
|
||||
"post renders body"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/welcome/"))) "Served by lib/host.")
|
||||
true)
|
||||
;; trailing slash optional — /welcome and /welcome/ both resolve
|
||||
(host-bl-test
|
||||
"no trailing slash also 200"
|
||||
(dream-status (host-bl-app (host-bl-req "/welcome")))
|
||||
200)
|
||||
|
||||
;; golden: endpoint body == the exact rendered HTML of the published post
|
||||
(host-bl-test
|
||||
"golden render"
|
||||
(dream-resp-body (host-bl-app (host-bl-req "/welcome/")))
|
||||
"<h1>Hello SX</h1><p>Served by lib/host.</p>")
|
||||
|
||||
;; persistence: the store holds 2 blocks (op-log replay), lookup materialises the
|
||||
;; doc from the store per call, and re-seeding is idempotent (no duplicate blocks).
|
||||
(host-bl-test "store has 2 blocks" (content/count (content/head host/blog-store "welcome")) 2)
|
||||
(host-bl-test "lookup materialises the doc" (content/count (host/blog-lookup "welcome")) 2)
|
||||
(host/blog-seed! "welcome" "Hello SX" "Served by lib/host." 2)
|
||||
(host-bl-test "re-seed is idempotent" (content/count (content/head host/blog-store "welcome")) 2)
|
||||
|
||||
;; ── unknown slug -> 404 ─────────────────────────────────────────────
|
||||
(host-bl-test
|
||||
"unknown slug 404"
|
||||
(dream-status (host-bl-app (host-bl-req "/nope/")))
|
||||
404)
|
||||
(host-bl-test
|
||||
"404 names the slug"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/nope/"))) "nope")
|
||||
(host-bl-test "post title in page"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/hello/"))) "<title>Hello World</title>")
|
||||
true)
|
||||
|
||||
;; ── route precedence: domain routes win over the :slug catch-all ────
|
||||
;; ── home + list ─────────────────────────────────────────────────────
|
||||
(host-bl-test "home lists post"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/"))) "href=\"/hello/\"")
|
||||
true)
|
||||
(host-bl-test "json list shows post"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/posts"))) "\"slug\":\"hello\"")
|
||||
true)
|
||||
(host-bl-test "GET /new shows form"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/new"))) "<form")
|
||||
true)
|
||||
|
||||
;; ── unknown + precedence ────────────────────────────────────────────
|
||||
(host-bl-test "unknown slug 404" (dream-status (host-bl-app (host-bl-req "/nope/"))) 404)
|
||||
(feed/reset!)
|
||||
(host-bl-test
|
||||
"/feed served by feed, not blog 404"
|
||||
(dream-status (host-bl-app (host-bl-req "/feed")))
|
||||
200)
|
||||
(host-bl-test
|
||||
"/feed body is the feed envelope, not HTML"
|
||||
(host-bl-test "/feed not captured by :slug"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/feed"))) "\"ok\":true")
|
||||
true)
|
||||
|
||||
;; ── CRUD: list / create / update / delete (writes auth+ACL guarded) ─
|
||||
;; ── writes: editor form ingest + JSON CRUD (auth+ACL) ───────────────
|
||||
(acl/load! (list (acl-grant "editor" "edit" "blog")))
|
||||
(define host-bl-resolve
|
||||
(fn (tok) (cond ((= tok "good") "editor") ((= tok "weak") "reader") (true nil))))
|
||||
(define host-bl-wapp
|
||||
(host/make-app (list (host/blog-write-routes host-bl-resolve) host/blog-routes)))
|
||||
(define host-bl-send
|
||||
(fn (method target auth body)
|
||||
(dream-request method target (if auth {:authorization auth} {}) body)))
|
||||
(fn (method target auth ctype body)
|
||||
(dream-request method target
|
||||
(merge (if auth {:authorization auth} {}) (if ctype {:content-type ctype} {})) body)))
|
||||
|
||||
;; start from a clean store
|
||||
(host/blog-use-store! (persist/open))
|
||||
|
||||
;; list empty
|
||||
(host-bl-test "list empty -> data:[]"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/posts" nil ""))) "\"data\":[]")
|
||||
true)
|
||||
;; HTML home page when empty
|
||||
(host-bl-test "home / -> 200 html"
|
||||
(contains? (dream-resp-header (host-bl-wapp (host-bl-send "GET" "/" nil "")) "content-type") "text/html")
|
||||
true)
|
||||
(host-bl-test "empty home says no posts"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/" nil ""))) "No posts yet")
|
||||
;; -- editor form ingest (form-urlencoded, the editor's submit shape) --
|
||||
(host-bl-test "form ingest no auth -> 401"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/new" nil
|
||||
"application/x-www-form-urlencoded" "title=X")))
|
||||
401)
|
||||
(host-bl-test "form ingest authed -> 303 redirect"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/new" "Bearer good"
|
||||
"application/x-www-form-urlencoded"
|
||||
"title=My+First+Post&sx_content=(article+(h1+%22My+First+Post%22)+(p+%22Hi%22))&status=published")))
|
||||
303)
|
||||
(host-bl-test "form ingest set Location to the new slug"
|
||||
(dream-resp-header
|
||||
(host-bl-wapp (host-bl-send "POST" "/new" "Bearer good"
|
||||
"application/x-www-form-urlencoded"
|
||||
"title=Another+One&sx_content=(p+%22x%22)&status=published"))
|
||||
"location")
|
||||
"/another-one/")
|
||||
(host-bl-test "ingested post renders"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-req "/my-first-post/"))) "<h1>My First Post</h1>")
|
||||
true)
|
||||
|
||||
;; create requires auth
|
||||
(host-bl-test "create no auth -> 401"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" nil "{}")))
|
||||
401)
|
||||
(host-bl-test "create authed-unpermitted -> 403"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer weak"
|
||||
"{\"slug\":\"hello\",\"title\":\"Hi\",\"body\":\"B\"}")))
|
||||
403)
|
||||
;; create permitted -> 201
|
||||
(host-bl-test "create -> 201"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer good"
|
||||
"{\"slug\":\"hello\",\"title\":\"Hello World\",\"body\":\"First post.\"}")))
|
||||
;; -- JSON CRUD --
|
||||
(host-bl-test "json create -> 201"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer good" "application/json"
|
||||
"{\"title\":\"Json Post\",\"sx_content\":\"(p \\\"jp\\\")\",\"status\":\"draft\"}")))
|
||||
201)
|
||||
;; created post renders at GET /<slug>/
|
||||
(host-bl-test "created post reads back as HTML"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/hello/" nil ""))) "<h1>Hello World</h1>")
|
||||
true)
|
||||
;; appears in the list
|
||||
(host-bl-test "list shows created post"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/posts" nil ""))) "Hello World")
|
||||
true)
|
||||
;; home page lists it with a link to /<slug>/
|
||||
(host-bl-test "home lists post title"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/" nil ""))) "Hello World")
|
||||
true)
|
||||
(host-bl-test "home links to the post"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/" nil ""))) "href=\"/hello/\"")
|
||||
true)
|
||||
;; create duplicate -> 409
|
||||
(host-bl-test "create duplicate -> 409"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer good"
|
||||
"{\"slug\":\"hello\",\"title\":\"X\",\"body\":\"Y\"}")))
|
||||
(host-bl-test "json create unpermitted -> 403"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer weak" "application/json"
|
||||
"{\"title\":\"Nope\"}")))
|
||||
403)
|
||||
(host-bl-test "json create duplicate -> 409"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer good" "application/json"
|
||||
"{\"slug\":\"json-post\",\"title\":\"Json Post\"}")))
|
||||
409)
|
||||
;; missing fields -> 400
|
||||
(host-bl-test "create missing fields -> 400"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer good" "{\"slug\":\"x\"}")))
|
||||
(host-bl-test "json create no title -> 400"
|
||||
(dream-status (host-bl-wapp (host-bl-send "POST" "/posts" "Bearer good" "application/json" "{}")))
|
||||
400)
|
||||
|
||||
;; update -> 200 and content changes
|
||||
(host-bl-test "update -> 200"
|
||||
(dream-status (host-bl-wapp (host-bl-send "PUT" "/posts/hello" "Bearer good"
|
||||
"{\"title\":\"Edited Title\",\"body\":\"Edited body.\"}")))
|
||||
(dream-status (host-bl-wapp (host-bl-send "PUT" "/posts/json-post" "Bearer good" "application/json"
|
||||
"{\"sx_content\":\"(p \\\"edited\\\")\"}")))
|
||||
200)
|
||||
(host-bl-test "update changed the rendered post"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/hello/" nil ""))) "<h1>Edited Title</h1>")
|
||||
(host-bl-test "update changed content"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-req "/json-post/"))) "edited")
|
||||
true)
|
||||
(host-bl-test "update missing post -> 404"
|
||||
(dream-status (host-bl-wapp (host-bl-send "PUT" "/posts/ghost" "Bearer good"
|
||||
"{\"title\":\"T\",\"body\":\"B\"}")))
|
||||
(host-bl-test "update missing -> 404"
|
||||
(dream-status (host-bl-wapp (host-bl-send "PUT" "/posts/ghost" "Bearer good" "application/json" "{}")))
|
||||
404)
|
||||
(host-bl-test "update no auth -> 401"
|
||||
(dream-status (host-bl-wapp (host-bl-send "PUT" "/posts/hello" nil "{}")))
|
||||
401)
|
||||
|
||||
;; delete -> 200, then gone (404) and absent from list
|
||||
(host-bl-test "delete -> 200"
|
||||
(dream-status (host-bl-wapp (host-bl-send "DELETE" "/posts/hello" "Bearer good" "")))
|
||||
(dream-status (host-bl-wapp (host-bl-send "DELETE" "/posts/json-post" "Bearer good" "" "")))
|
||||
200)
|
||||
(host-bl-test "deleted post -> 404"
|
||||
(dream-status (host-bl-wapp (host-bl-send "GET" "/hello/" nil "")))
|
||||
404)
|
||||
(host-bl-test "deleted post gone from list"
|
||||
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/posts" nil ""))) "hello")
|
||||
false)
|
||||
(host-bl-test "deleted -> 404" (dream-status (host-bl-wapp (host-bl-req "/json-post/"))) 404)
|
||||
(host-bl-test "delete missing -> 404"
|
||||
(dream-status (host-bl-wapp (host-bl-send "DELETE" "/posts/ghost" "Bearer good" "")))
|
||||
(dream-status (host-bl-wapp (host-bl-send "DELETE" "/posts/ghost" "Bearer good" "" "")))
|
||||
404)
|
||||
|
||||
(define
|
||||
host-bl-tests-run!
|
||||
(fn
|
||||
()
|
||||
(fn ()
|
||||
{:total (+ host-bl-pass host-bl-fail)
|
||||
:passed host-bl-pass
|
||||
:failed host-bl-fail
|
||||
:fails host-bl-fails}))
|
||||
:passed host-bl-pass :failed host-bl-fail :fails host-bl-fails}))
|
||||
|
||||
Reference in New Issue
Block a user