summaryrefslogtreecommitdiffstats
path: root/tests/suite.lisp
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:42:28 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:42:28 -0300
commitae9ad8a4c61b793f6859c84dac80b1c1b04b7b68 (patch)
tree62271e7672dfc58f59dda2ea4dc539c89a1ea5ca /tests/suite.lisp
downloadlisp-ae9ad8a4c61b793f6859c84dac80b1c1b04b7b68.tar.gz
Initial importv0.1.0
Diffstat (limited to 'tests/suite.lisp')
-rw-r--r--tests/suite.lisp259
1 files changed, 259 insertions, 0 deletions
diff --git a/tests/suite.lisp b/tests/suite.lisp
new file mode 100644
index 0000000..4ef0c5f
--- /dev/null
+++ b/tests/suite.lisp
@@ -0,0 +1,259 @@
+(in-package #:cepis/tests)
+
+(def-suite cepis
+ :description "The whole cepis suite, run against a local HTTP server.")
+
+(in-suite cepis)
+
+(defparameter +se-fields+
+ "\"cep\":\"01001000\",\"logradouro\":\"Praça da Sé\",\"complemento\":\"lado ímpar\",\"unidade\":\"\",\"bairro\":\"Sé\",\"localidade\":\"São Paulo\",\"uf\":\"SP\",\"estado\":\"São Paulo\",\"regiao\":\"Sudeste\",\"ibge\":\"3550308\",\"gia\":\"1004\",\"ddd\":\"11\",\"siafi\":\"7107\"")
+
+(defparameter +csv-header+
+ "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi")
+
+(defun se-json (&rest extra-fields)
+ "The fixture address, with EXTRA-FIELDS spliced in as raw JSON members."
+ (format nil "{~a~{,~a~}}" +se-fields+ extra-fields))
+
+(defun csv-body (&rest rows)
+ (format nil "~a~%~{~a~%~}" +csv-header+ rows))
+
+(defmacro signalled-message (condition-type &body body)
+ "Run BODY and return the message of the CONDITION-TYPE it signals, or NIL."
+ `(handler-case (progn ,@body nil)
+ (,condition-type (condition) (error-message condition))))
+
+(test normalisation-accepts-masked-and-unmasked
+ (dolist (value '("01001000" "01001-000" " 01001-000 " "01001.000"))
+ (is (string= "01001000" (normalize-cep value)))))
+
+(test normalisation-rejects-bad-format
+ (dolist (value '("" "123" "0100100a" "010010000" "abcdefgh"))
+ (signals invalid-cep (normalize-cep value))))
+
+(test normalisation-rejects-wrong-type
+ (signals invalid-cep (normalize-cep 1001000)))
+
+(test normalisation-is-a-parse-error
+ (signals parse-error (normalize-cep "123")))
+
+(test successful-lookup
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 200 (se-json)))
+ (let ((address (lookup "01001-000" :client client)))
+ (is (string= "São Paulo" (address-localidade address)))
+ (is (string= "SP" (address-uf address)))
+ (is (string= "01001-000" (address-formatted-cep address)))
+ (is (null (address-geo address))))))
+
+(test reads-the-coordinate-when-present
+ (with-fake-server (server client)
+ (respond server "/01310100"
+ (reply 200 (se-json "\"geo\":{\"lat\":-23.5,\"lon\":-46.6,\"raio_m\":120,\"precisao\":\"logradouro\"}")))
+ (let ((geo (address-geo (lookup "01310100" :client client))))
+ (is (< (abs (- -23.5d0 (geo-lat geo))) 1d-9))
+ (is (= 120 (geo-raio-m geo)))
+ (is (geo-precise-p geo)))))
+
+(test municipality-coordinate-is-not-precise
+ (with-fake-server (server client)
+ (respond server "/01001000"
+ (reply 200 (se-json "\"geo\":{\"lat\":-23.5,\"lon\":-46.6,\"raio_m\":22000,\"precisao\":\"localidade\"}")))
+ (let ((geo (address-geo (lookup "01001000" :client client))))
+ (is (not (geo-precise-p geo))))))
+
+(test malformed-geo-does-not-lose-the-address
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 200 (se-json "\"geo\":{\"lat\":\"not a number\"}")))
+ (let ((address (lookup "01001000" :client client)))
+ (is (null (address-geo address)))
+ (is (string= "São Paulo" (address-localidade address))))))
+
+(test new-api-field-does-not-break
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 200 (se-json "\"campo_do_futuro\":\"value\"")))
+ (let ((address (lookup "01001000" :client client)))
+ (is (string= "value" (gethash "campo_do_futuro" (address-raw address)))))))
+
+(test not-found
+ (with-fake-server (server client)
+ (respond server "/99999999"
+ (reply 404 "{\"erro\":\"cep_nao_encontrado\",\"mensagem\":\"gone\"}"))
+ (signals cep-not-found (lookup "99999999" :client client))
+ (handler-case (lookup "99999999" :client client)
+ (cep-not-found (condition)
+ (is (string= "99999999" (cell-error-name condition)))))))
+
+(test not-found-is-not-retried
+ (with-fake-server (server client)
+ (respond server "/99999999" (reply 404 "{\"erro\":\"cep_nao_encontrado\"}"))
+ (signals cep-not-found (lookup "99999999" :client client))
+ (is (= 1 (call-count server "/99999999")))))
+
+(test invalid-cep-never-leaves-the-machine
+ (with-fake-server (server client)
+ (signals invalid-cep (lookup "123" :client client))
+ (is (zerop (total-calls server)))))
+
+(test api-400-becomes-invalid-cep
+ (with-fake-server (server client)
+ (respond server "/00000000"
+ (reply 400 "{\"erro\":\"cep_invalido\",\"mensagem\":\"rejected by the API\"}"))
+ (is (search "rejected by the API"
+ (or (signalled-message invalid-cep (lookup "00000000" :client client)) "")))))
+
+(test response-that-is-not-json
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 200 "<html>captive portal</html>"))
+ (signals invalid-response (lookup "01001000" :client client))))
+
+(test retries-on-500-then-succeeds
+ (with-fake-server (server client)
+ (respond server "/01001000"
+ (list :sequence
+ (reply 500 "{\"erro\":\"interno\"}")
+ (reply 200 (se-json))))
+ (is (string= "São Paulo" (address-localidade (lookup "01001000" :client client))))
+ (is (= 2 (call-count server "/01001000")))))
+
+(test exhausts-attempts-then-signals
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 503 "{\"erro\":\"interno\"}"))
+ (handler-case (progn (lookup "01001000" :client client) (fail "expected a server-error"))
+ (server-error (condition)
+ (is (= 503 (server-error-status condition)))))
+ (is (= 3 (call-count server "/01001000")))))
+
+(test max-attempts-one-disables-retry
+ (with-fake-server (server client :max-attempts 1)
+ (respond server "/01001000" (reply 500 "{}"))
+ (signals server-error (lookup "01001000" :client client))
+ (is (= 1 (call-count server "/01001000")))))
+
+(test 429-becomes-rate-limited-with-retry-after
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 429 "{}" '(("Retry-After" . "7"))))
+ (handler-case (progn (lookup "01001000" :client client) (fail "expected a rate-limited"))
+ (rate-limited (condition)
+ (is (= 7 (rate-limited-retry-after condition)))))))
+
+(test retry-after-honours-the-cap
+ (with-fake-server (server client)
+ (respond server "/01001000" (reply 429 "{}" '(("Retry-After" . "3600"))))
+ (let ((started (get-internal-real-time)))
+ (signals rate-limited (lookup "01001000" :client client))
+ (is (< (/ (- (get-internal-real-time) started)
+ internal-time-units-per-second)
+ 5)))))
+
+(test network-error-when-nothing-listens
+ (let ((client (make-client :base-url "http://127.0.0.1:1"
+ :max-attempts 2
+ :backoff-base 0.001
+ :backoff-max 0.01
+ :timeout 1.0)))
+ (signals network-error (lookup "01001000" :client client))))
+
+(test total-timeout-bounds-the-whole-call
+ (with-fake-server (server client
+ :max-attempts 50 :backoff-base 0.05
+ :backoff-max 0.05 :total-timeout 0.3)
+ (respond server "/01001000" (reply 500 "{}"))
+ (let ((started (get-internal-real-time)))
+ (signals server-error (lookup "01001000" :client client))
+ (is (< (/ (- (get-internal-real-time) started)
+ internal-time-units-per-second)
+ 5))
+ (is (< (call-count server "/01001000") 50)))))
+
+(test batch-preserves-order-and-marks-missing
+ (with-fake-server (server client)
+ (respond server "/csv?ceps=01001000%2C99999999%2C20040020"
+ (reply 200 (csv-body
+ "01001000,Praça da Sé,lado ímpar,,Sé,São Paulo,SP,São Paulo,Sudeste,3550308,1004,11,7107"
+ "99999999,,,,,,,,,,,,"
+ "20040020,Avenida Rio Branco,,,Centro,Rio de Janeiro,RJ,Rio de Janeiro,Sudeste,3304557,,21,6001")))
+ (let ((result (lookup-many '("01001000" "99999999" "20040020") :client client)))
+ (is (= 3 (length result)))
+ (is (string= "São Paulo" (address-localidade (first result))))
+ (is (null (second result)))
+ (is (string= "RJ" (address-uf (third result)))))))
+
+(test batch-field-with-comma-is-parsed
+ (with-fake-server (server client)
+ (respond server "/csv?ceps=01001000"
+ (reply 200 (csv-body
+ "01001000,\"Rua Um, Dois\",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1")))
+ (let ((result (lookup-many '("01001000") :client client)))
+ (is (string= "Rua Um, Dois" (address-logradouro (first result)))))))
+
+(test batch-carries-no-coordinate
+ (with-fake-server (server client)
+ (respond server "/csv?ceps=01001000"
+ (reply 200 (csv-body
+ "01001000,Praça da Sé,,,Sé,São Paulo,SP,São Paulo,Sudeste,1,,11,1")))
+ (is (null (address-geo (first (lookup-many '("01001000") :client client)))))))
+
+(test batch-empty-list-makes-no-request
+ (with-fake-server (server client)
+ (is (null (lookup-many '() :client client)))
+ (is (zerop (total-calls server)))))
+
+(test batch-chunks-above-the-api-limit
+ (with-fake-server (server client)
+ (let ((ceps (loop for index from 0 below 501 collect (format nil "~8,'0d" index)))
+ (calls 0))
+ (setf (fake-server-handler server)
+ (lambda (path)
+ (declare (ignore path))
+ (incf calls)
+ (reply 200 (apply #'csv-body
+ (mapcar (lambda (cep)
+ (format nil "~a,,,,,X,SP,São Paulo,Sudeste,1,,11,1" cep))
+ (if (= calls 1)
+ (subseq ceps 0 500)
+ (subseq ceps 500)))))))
+ (let ((result (lookup-many ceps :client client)))
+ (is (= 2 calls))
+ (is (= 501 (length result)))))))
+
+(test batch-truncated-response-does-not-shift
+ (with-fake-server (server client)
+ (respond server "/csv?ceps=01001000%2C20040020"
+ (reply 200 (csv-body
+ "01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1")))
+ (let ((result (lookup-many '("01001000" "20040020") :client client)))
+ (is (= 2 (length result)))
+ (is (null (second result))))))
+
+(test reads-health
+ (with-fake-server (server client)
+ (respond server "/health"
+ (reply 200 "{\"status\":\"ok\",\"dne\":\"26071\",\"ceps\":1596168,\"geo\":894489,\"base_gerada_em\":\"2026-07-24T03:48:28Z\"}"))
+ (let ((health (health :client client)))
+ (is (health-ok-p health))
+ (is (string= "26071" (health-dne health)))
+ (is (= 1596168 (health-ceps health)))
+ (is (string= "2026-07-24T03:48:28Z" (health-base-gerada-em health))))))
+
+(test degraded-health-does-not-signal
+ (with-fake-server (server client)
+ (respond server "/health"
+ (reply 503 "{\"status\":\"degradado\",\"dne\":\"\",\"ceps\":0,\"geo\":0}"))
+ (let ((health (health :client client)))
+ (is (not (health-ok-p health)))
+ (is (= 1 (call-count server "/health"))))))
+
+(test trailing-slash-is-not-duplicated
+ (is (string= "http://x" (client-base-url (make-client :base-url "http://x/")))))
+
+(test invalid-parameters-are-rejected
+ (signals error (make-client :max-attempts 0))
+ (signals error (make-client :timeout 0)))
+
+(test user-agent-identifies-the-library
+ (is (search "cepis-lisp/" (client-user-agent (make-client)))))
+
+(defun run-tests ()
+ "Run the whole suite. Returns true when every test passed."
+ (run! 'cepis))