Files
mcias/clients/lisp/tests/client-tests.lisp
Kyle Isom 0c441f5c4f 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
2026-03-11 16:38:32 -07:00

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)))