safsaf/tests/test-router.scm
Christopher Baines 5b0e6397dc
All checks were successful
/ test (push) Successful in 9s
Initial commit
Safsaf is a Guile web framework, written using Claude Code running
Claude Opus 4.6, based off of the Guix Data Service, Nar Herder and
Guix Build Coordinator codebases.
2026-04-13 14:24:19 +03:00

188 lines
7.2 KiB
Scheme

;; Safsaf, a Guile web framework
;; Copyright (C) 2026 Christopher Baines <mail@cbaines.net>
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU Lesser General Public License
;; as published by the Free Software Foundation, either version 3 of
;; the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; Lesser General Public License for more details.
;;
;; You should have received a copy of the GNU Lesser General Public
;; License along with this program. If not, see
;; <https://www.gnu.org/licenses/>.
;;; test-router.scm — Tests for (safsaf router)
(use-modules (tests support)
(safsaf router)
(srfi srfi-71))
;; Every compile-routes call needs a catch-all as the last route.
(define catch-all
(route '* '(. rest) (lambda (r) (values 'not-found #f))))
(define (match-path routes method path)
"Compile ROUTES (appending catch-all), match METHOD and PATH segments."
(let ((compiled _rr (compile-routes (append routes (list catch-all)))))
(let ((handler bindings (match-route compiled method path)))
(values handler bindings))))
(define-suite router-tests
(suite "match-route"
(test "literal path"
(define h (lambda (r) 'ok))
(define routes (list (route 'GET '("users" "list") h)))
(let ((handler bindings (match-path routes 'GET '("users" "list"))))
(is (eq? h handler))
(is (equal? '() bindings))))
(test "no match falls through to catch-all"
(define h (lambda (r) 'ok))
(define routes (list (route 'GET '("users") h)))
(let ((handler _bindings (match-path routes 'GET '("other"))))
(is (not (eq? h handler)))))
(test "capture segment"
(define h (lambda (r) 'ok))
(define routes (list (route 'GET '("users" id) h)))
(let ((_handler bindings (match-path routes 'GET '("users" "42"))))
(is (equal? "42" (assq-ref bindings 'id)))))
(test "wildcard rest"
(define h (lambda (r) 'ok))
(define routes (list (route 'GET '("files" . path) h)))
(let ((_handler bindings (match-path routes 'GET '("files" "a" "b"))))
(is (equal? '("a" "b") (assq-ref bindings 'path)))))
(test "predicate segment"
(define h (lambda (r) 'ok))
(define routes
(list (route 'GET `("items" (,string->number id)) h)))
(let ((handler _b (match-path routes 'GET '("items" "99"))))
(is (eq? h handler)))
(let ((handler _b (match-path routes 'GET '("items" "abc"))))
(is (not (eq? h handler)))))
(test "method filtering"
(define h-get (lambda (r) 'get))
(define h-post (lambda (r) 'post))
(define routes (list (route 'GET '("x") h-get)
(route 'POST '("x") h-post)))
(let ((handler _b (match-path routes 'GET '("x"))))
(is (eq? h-get handler)))
(let ((handler _b (match-path routes 'POST '("x"))))
(is (eq? h-post handler))))
(test "multi-method route"
(define h (lambda (r) 'ok))
(define routes (list (route '(GET HEAD) '("x") h)))
(let ((handler _b (match-path routes 'GET '("x"))))
(is (eq? h handler)))
(let ((handler _b (match-path routes 'HEAD '("x"))))
(is (eq? h handler)))
(let ((handler _b (match-path routes 'POST '("x"))))
(is (not (eq? h handler))))))
(suite "route-group"
(test "prefix nesting"
(define h (lambda (r) 'ok))
(define routes
(list (route-group '("api")
(route 'GET '("users") h #:name 'api-users))))
(let ((handler _b (match-path routes 'GET '("api" "users"))))
(is (eq? h handler)))
(let ((handler _b (match-path routes 'GET '("users"))))
(is (not (eq? h handler))))))
(suite "wrap-routes"
(test "wrapper ordering"
;; First wrapper = outermost = runs first on request.
;; We verify by building a call log.
(define log '())
(define (make-wrapper tag)
(lambda (handler)
(lambda (request)
(set! log (append log (list tag)))
(handler request))))
(define h (lambda (r) (set! log (append log '(handler))) 'ok))
(define r (route 'GET '("x") h))
(wrap-routes (list r) (make-wrapper 'a) (make-wrapper 'b))
((route-handler r) 'fake-request)
(is (equal? '(a b handler) log))))
(suite "find-allowed-methods"
(test "returns methods for path-matched routes"
(define routes
(list (route 'GET '("users") identity)
(route 'POST '("users") identity)))
(let ((compiled _rr (compile-routes (append routes (list catch-all)))))
(is (equal? '(POST GET)
(find-allowed-methods compiled '("users"))))))
(test "returns empty for unmatched path"
(define routes (list (route 'GET '("users") identity)))
(let ((compiled _rr (compile-routes (append routes (list catch-all)))))
(is (equal? '() (find-allowed-methods compiled '("other"))))))
(test "collects from multi-method routes"
(define routes (list (route '(GET HEAD) '("x") identity)
(route 'POST '("x") identity)))
(let ((compiled _rr (compile-routes (append routes (list catch-all)))))
(is (equal? '(POST HEAD GET)
(find-allowed-methods compiled '("x"))))))
(test "deduplicates methods"
(define routes (list (route 'GET '("x") identity)
(route 'GET '("x") identity)))
(let ((compiled _rr (compile-routes (append routes (list catch-all)))))
(is (equal? '(GET)
(find-allowed-methods compiled '("x"))))))
(test "excludes catch-all from scan"
(let ((compiled _rr (compile-routes (list catch-all))))
(is (equal? '() (find-allowed-methods compiled '("anything")))))))
(suite "path-for"
(test "simple and parameterised"
(define grp
(route-group '()
(route 'GET '("users") identity #:name 'users)
(route 'GET '("users" id) identity #:name 'user)))
(let ((_compiled rr
(compile-routes (list grp catch-all))))
(parameterize ((current-reverse-routes rr))
(is (equal? "/users" (path-for grp 'users)))
(is (equal? "/users/42" (path-for grp 'user '((id . "42"))))))))
(test "query and fragment"
(define grp
(route-group '()
(route 'GET '("search") identity #:name 'search)))
(let ((_compiled rr
(compile-routes (list grp catch-all))))
(parameterize ((current-reverse-routes rr))
(is (equal? "/search?q=hello"
(path-for grp 'search '() #:query '((q . "hello")))))
(is (equal? "/search#top"
(path-for grp 'search '() #:fragment "top"))))))
(test "scoped lookup in group"
(define grp
(route-group '("api") #:name 'api
(route 'GET '("items") identity #:name 'items)))
(let ((_compiled rr
(compile-routes (list grp catch-all))))
(parameterize ((current-reverse-routes rr))
(is (equal? "/api/items" (path-for grp 'items))))))))
(run-tests router-tests)