Composable handler->handler layers over Dream's primitives, with auth and permission POLICY injected so the layer is policy-free and testable: - middleware.sx: host/wrap-errors (JSON 500 via dream-catch-with), host/require-auth (bearer->principal via dream-bearer-token, JSON 401, injected token resolver), host/require-permission (lib/acl acl/permit? gate, JSON 403, injected resource extractor), host/pipeline (first = outermost) - feed.sx: POST /feed via host/feed-write-routes — auth ∘ ACL(post,feed) ∘ wrap-errors over host/feed-create (parse JSON body -> feed/post -> 201; non-object -> 400). Created activity reads back via GET /feed. - middleware suite (9) + feed write tests (6 new); conformance preloads now include the Datalog engine + ACL subsystem + Dream auth/error. ACL works with string atoms (no symbol coercion). Mute/prefs layer and sxtp.sx deferred to the next tick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.3 KiB
Plaintext
108 lines
3.3 KiB
Plaintext
;; lib/host/tests/middleware.sx — auth (bearer -> principal), ACL gate, and error
|
|
;; trapping, composed via host/pipeline. ACL facts: alice may "post" on "feed".
|
|
|
|
(define host-mw-pass 0)
|
|
(define host-mw-fail 0)
|
|
(define host-mw-fails (list))
|
|
|
|
(define
|
|
host-mw-test
|
|
(fn
|
|
(name actual expected)
|
|
(if
|
|
(= actual expected)
|
|
(set! host-mw-pass (+ host-mw-pass 1))
|
|
(begin
|
|
(set! host-mw-fail (+ host-mw-fail 1))
|
|
(append! host-mw-fails {:name name :actual actual :expected expected})))))
|
|
|
|
;; ── fixtures ───────────────────────────────────────────────────────
|
|
(acl/load! (list (acl-grant "alice" "post" "feed")))
|
|
|
|
(define host-mw-resolve
|
|
(fn (tok) (if (= tok "good") "alice" nil)))
|
|
|
|
(define host-mw-handler
|
|
(fn (req) (host/ok-status 201 (host/principal req))))
|
|
|
|
;; protected: needs auth + post/feed permission
|
|
(define host-mw-protected
|
|
(host/pipeline
|
|
(list
|
|
(host/require-auth host-mw-resolve)
|
|
(host/require-permission "post" (fn (req) "feed")))
|
|
host-mw-handler))
|
|
|
|
;; protected with an action alice is NOT granted
|
|
(define host-mw-protected-del
|
|
(host/pipeline
|
|
(list
|
|
(host/require-auth host-mw-resolve)
|
|
(host/require-permission "delete" (fn (req) "feed")))
|
|
host-mw-handler))
|
|
|
|
(define
|
|
host-mw-req
|
|
(fn (auth)
|
|
(dream-request "POST" "/feed"
|
|
(if auth {:authorization auth} {})
|
|
"")))
|
|
|
|
;; ── auth ───────────────────────────────────────────────────────────
|
|
(host-mw-test
|
|
"no token -> 401"
|
|
(dream-status (host-mw-protected (host-mw-req nil)))
|
|
401)
|
|
(host-mw-test
|
|
"401 has www-authenticate"
|
|
(dream-resp-header (host-mw-protected (host-mw-req nil)) "www-authenticate")
|
|
"Bearer")
|
|
(host-mw-test
|
|
"bad token -> 401"
|
|
(dream-status (host-mw-protected (host-mw-req "Bearer wrong")))
|
|
401)
|
|
|
|
;; ── authz ──────────────────────────────────────────────────────────
|
|
(host-mw-test
|
|
"authed + permitted -> 201"
|
|
(dream-status (host-mw-protected (host-mw-req "Bearer good")))
|
|
201)
|
|
(host-mw-test
|
|
"principal threaded to handler"
|
|
(contains?
|
|
(dream-resp-body (host-mw-protected (host-mw-req "Bearer good")))
|
|
"\"data\":\"alice\"")
|
|
true)
|
|
(host-mw-test
|
|
"authed but not permitted -> 403"
|
|
(dream-status (host-mw-protected-del (host-mw-req "Bearer good")))
|
|
403)
|
|
(host-mw-test
|
|
"403 envelope"
|
|
(contains?
|
|
(dream-resp-body (host-mw-protected-del (host-mw-req "Bearer good")))
|
|
"\"error\":\"forbidden\"")
|
|
true)
|
|
|
|
;; ── error trapping ─────────────────────────────────────────────────
|
|
(define host-mw-boom (fn (req) (error "kaboom")))
|
|
(host-mw-test
|
|
"wrap-errors -> 500"
|
|
(dream-status ((host/wrap-errors host-mw-boom) (host-mw-req nil)))
|
|
500)
|
|
(host-mw-test
|
|
"500 envelope"
|
|
(contains?
|
|
(dream-resp-body ((host/wrap-errors host-mw-boom) (host-mw-req nil)))
|
|
"\"ok\":false")
|
|
true)
|
|
|
|
(define
|
|
host-mw-tests-run!
|
|
(fn
|
|
()
|
|
{:total (+ host-mw-pass host-mw-fail)
|
|
:passed host-mw-pass
|
|
:failed host-mw-fail
|
|
:fails host-mw-fails}))
|