Implement Phase 9: client libraries (Go, Rust, Lisp, Python)
- 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
This commit is contained in:
201
clients/lisp/tests/client-tests.lisp
Normal file
201
clients/lisp/tests/client-tests.lisp
Normal file
@@ -0,0 +1,201 @@
|
||||
;;;; 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)))
|
||||
Reference in New Issue
Block a user