- clients/README.md: canonical API surface and error type reference - clients/testdata/: shared JSON response fixtures - clients/go/: mciasgoclient package; net/http + TLS 1.2+; sync.RWMutex token state; DisallowUnknownFields on all decoders; 25 tests pass - clients/rust/: async mcias-client crate; reqwest+rustls (no OpenSSL); thiserror MciasError enum; Arc<RwLock> token state; 22+1 tests pass; cargo clippy -D warnings clean - clients/lisp/: ASDF mcias-client; dexador HTTP, yason JSON; mcias-error condition hierarchy; Hunchentoot mock-dispatcher; 37 fiveam checks pass on SBCL 2.6.1; yason boolean normalisation in validate-token - clients/python/: mcias_client package (Python 3.11+); httpx sync; py.typed; dataclasses; 32 pytest tests; mypy --strict + ruff clean - test/mock/mockserver.go: in-memory mock server for Go client tests - ARCHITECTURE.md §19: updated per-language notes to match implementation - PROGRESS.md: Phase 9 marked complete - .gitignore: exclude clients/rust/target/, python .venv, .pytest_cache, .fasl files Security: token never logged or exposed in error messages in any library; TLS enforced in all four languages; token stored under lock/mutex/RwLock
202 lines
7.7 KiB
Common Lisp
202 lines
7.7 KiB
Common Lisp
;;;; tests/client-tests.lisp -- fiveam test suite for mcias-client
|
|
|
|
(in-package #:mcias-client-tests)
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Test suite
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:def-suite mcias-client-suite
|
|
:description "Tests for the mcias-client library")
|
|
|
|
(fiveam:in-suite mcias-client-suite)
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Helper macro
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(defmacro with-mock-server ((client-var &key admin-token) &body body)
|
|
"Spin up a fresh mock server, bind CLIENT-VAR, run BODY, then stop."
|
|
(let ((port-var (gensym "PORT"))
|
|
(server-url (gensym "URL")))
|
|
`(let* ((,port-var (start-mock-server))
|
|
(,server-url (format nil "http://localhost:~A" ,port-var))
|
|
(,client-var (make-client ,server-url :token ,admin-token)))
|
|
(unwind-protect
|
|
(progn ,@body)
|
|
(stop-mock-server)))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Condition hierarchy tests
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test condition-hierarchy
|
|
"Verify the condition type hierarchy."
|
|
(fiveam:is (subtypep 'mcias-auth-error 'mcias-error))
|
|
(fiveam:is (subtypep 'mcias-forbidden-error 'mcias-error))
|
|
(fiveam:is (subtypep 'mcias-not-found-error 'mcias-error))
|
|
(fiveam:is (subtypep 'mcias-input-error 'mcias-error))
|
|
(fiveam:is (subtypep 'mcias-conflict-error 'mcias-error))
|
|
(fiveam:is (subtypep 'mcias-server-error 'mcias-error)))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; make-client tests
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test make-client-basic
|
|
"make-client stores base-url and token."
|
|
(let ((c (make-client "http://localhost:9000" :token "tok123")))
|
|
(fiveam:is (string= "http://localhost:9000" (client-base-url c)))
|
|
(fiveam:is (string= "tok123" (client-token c)))))
|
|
|
|
(fiveam:test make-client-strips-trailing-slash
|
|
"make-client trims trailing slashes from the URL."
|
|
(let ((c (make-client "http://localhost:9000///")))
|
|
(fiveam:is (string= "http://localhost:9000" (client-base-url c)))))
|
|
|
|
(fiveam:test make-client-no-token
|
|
"make-client with no :token gives NIL token."
|
|
(let ((c (make-client "http://localhost:9000")))
|
|
(fiveam:is (null (client-token c)))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Server info tests
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test health-ok
|
|
"health returns T for a live server."
|
|
(with-mock-server (c)
|
|
(fiveam:is (eq t (health c)))))
|
|
|
|
(fiveam:test get-public-key
|
|
"get-public-key returns a plist with :kty :crv :x."
|
|
(with-mock-server (c)
|
|
(let ((jwk (get-public-key c)))
|
|
(fiveam:is (string= "OKP" (getf jwk :kty)))
|
|
(fiveam:is (string= "Ed25519" (getf jwk :crv)))
|
|
(fiveam:is (stringp (getf jwk :x))))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Authentication tests
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test login-success
|
|
"Successful login returns a token and stores it in the client."
|
|
(with-mock-server (c)
|
|
(multiple-value-bind (token expires-at)
|
|
(login c "admin" "adminpass")
|
|
(fiveam:is (stringp token))
|
|
(fiveam:is (stringp expires-at))
|
|
(fiveam:is (string= token (client-token c))))))
|
|
|
|
(fiveam:test login-bad-password
|
|
"Wrong password signals mcias-auth-error."
|
|
(with-mock-server (c)
|
|
(fiveam:signals mcias-auth-error
|
|
(login c "admin" "wrongpassword"))))
|
|
|
|
(fiveam:test login-unknown-user
|
|
"Unknown username signals mcias-auth-error."
|
|
(with-mock-server (c)
|
|
(fiveam:signals mcias-auth-error
|
|
(login c "nosuchuser" "whatever"))))
|
|
|
|
(fiveam:test logout-clears-token
|
|
"logout revokes the token server-side and sets client-token to NIL."
|
|
(with-mock-server (c)
|
|
(login c "admin" "adminpass")
|
|
(fiveam:is (stringp (client-token c)))
|
|
(fiveam:is (eq t (logout c)))
|
|
(fiveam:is (null (client-token c)))))
|
|
|
|
(fiveam:test renew-token
|
|
"renew-token replaces the stored token."
|
|
(with-mock-server (c)
|
|
(login c "admin" "adminpass")
|
|
(let ((old-token (client-token c)))
|
|
(multiple-value-bind (new-token expires-at)
|
|
(renew-token c)
|
|
(fiveam:is (stringp new-token))
|
|
(fiveam:is (stringp expires-at))
|
|
(fiveam:is (not (string= old-token new-token)))
|
|
(fiveam:is (string= new-token (client-token c)))))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Token validation tests
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test validate-token-valid
|
|
"validate-token returns :valid T for a live token."
|
|
(with-mock-server (c)
|
|
(multiple-value-bind (token _expires)
|
|
(login c "admin" "adminpass")
|
|
(declare (ignore _expires))
|
|
(let ((result (validate-token c token)))
|
|
(fiveam:is (eq t (getf result :valid)))
|
|
(fiveam:is (stringp (getf result :sub)))))))
|
|
|
|
(fiveam:test validate-token-after-logout
|
|
"validate-token returns :valid NIL for a revoked token (not an error)."
|
|
(with-mock-server (c)
|
|
(login c "admin" "adminpass")
|
|
(let ((token (client-token c)))
|
|
(logout c)
|
|
(let ((result (validate-token c token)))
|
|
(fiveam:is (null (getf result :valid)))))))
|
|
|
|
(fiveam:test validate-token-garbage
|
|
"validate-token returns :valid NIL for a garbage token string."
|
|
(with-mock-server (c)
|
|
(let ((result (validate-token c "garbage-token-xyz")))
|
|
(fiveam:is (null (getf result :valid))))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Account management tests
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test create-account
|
|
"create-account returns a plist with :id :username :status."
|
|
(with-mock-server (c)
|
|
(login c "admin" "adminpass")
|
|
(let ((acct (create-account c "newuser" "user" :password "pass123")))
|
|
(fiveam:is (stringp (getf acct :id)))
|
|
(fiveam:is (string= "newuser" (getf acct :username)))
|
|
(fiveam:is (stringp (getf acct :status))))))
|
|
|
|
(fiveam:test list-accounts
|
|
"list-accounts returns a list with at least the admin account."
|
|
(with-mock-server (c)
|
|
(login c "admin" "adminpass")
|
|
(let ((accounts (list-accounts c)))
|
|
(fiveam:is (listp accounts))
|
|
(fiveam:is (>= (length accounts) 1)))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; End-to-end lifecycle test
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(fiveam:test e2e-login-validate-logout
|
|
"Full lifecycle: login -> validate (valid) -> logout -> validate (invalid)."
|
|
(with-mock-server (c)
|
|
(multiple-value-bind (token _)
|
|
(login c "admin" "adminpass")
|
|
(declare (ignore _))
|
|
;; Token should be valid right after login
|
|
(let ((r1 (validate-token c token)))
|
|
(fiveam:is (eq t (getf r1 :valid))))
|
|
;; Logout revokes the token
|
|
(logout c)
|
|
;; Token should now be invalid (not an error)
|
|
(let ((r2 (validate-token c token)))
|
|
(fiveam:is (null (getf r2 :valid)))))))
|
|
|
|
;;;; -----------------------------------------------------------------------
|
|
;;;; Entry point
|
|
;;;; -----------------------------------------------------------------------
|
|
|
|
(defun run-all-tests ()
|
|
"Run all mcias-client tests. Returns T if all pass."
|
|
(let ((results (fiveam:run 'mcias-client-suite)))
|
|
(fiveam:explain! results)
|
|
(fiveam:results-status results)))
|