diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/abi_probe.S | 102 | ||||
| -rwxr-xr-x | tests/run.sh | 32 | ||||
| -rwxr-xr-x | tests/server.py | 222 | ||||
| -rw-r--r-- | tests/test_cepis.c | 721 |
4 files changed, 1077 insertions, 0 deletions
diff --git a/tests/abi_probe.S b/tests/abi_probe.S new file mode 100644 index 0000000..0e07ec4 --- /dev/null +++ b/tests/abi_probe.S @@ -0,0 +1,102 @@ +/* + * cepis_abi_probe(fn, a0, a1, a2, a3) + * + * Calls fn(a0, a1, a2, a3) with every callee-saved register holding a + * distinctive sentinel and returns a bitmask of the ones it came back + * clobbered, plus bits for a moved stack pointer and a left-over direction + * flag. 0 means the callee honoured the System V AMD64 ABI. + * + * Not part of the library: this file is only linked into the test driver. + */ + + .text + .globl cepis_abi_probe + .type cepis_abi_probe, @function + .p2align 4 +cepis_abi_probe: + push %rbp + mov %rsp, %rbp + push %rbx + push %r12 + push %r13 + push %r14 + push %r15 + sub $8, %rsp + mov %rbp, cepis__probe_bp(%rip) + mov %rsp, cepis__probe_sp(%rip) + mov %rdi, %rax + mov %rsi, %rdi + mov %rdx, %rsi + mov %rcx, %rdx + mov %r8, %rcx + movabs $0x1111111111111111, %rbx + movabs $0x2222222222222222, %r12 + movabs $0x3333333333333333, %r13 + movabs $0x4444444444444444, %r14 + movabs $0x5555555555555555, %r15 + movabs $0x6666666666666666, %rbp + call *%rax + xor %r8d, %r8d + movabs $0x1111111111111111, %r9 + cmp %r9, %rbx + je .Lp_r12 + or $1, %r8d +.Lp_r12: + movabs $0x2222222222222222, %r9 + cmp %r9, %r12 + je .Lp_r13 + or $2, %r8d +.Lp_r13: + movabs $0x3333333333333333, %r9 + cmp %r9, %r13 + je .Lp_r14 + or $4, %r8d +.Lp_r14: + movabs $0x4444444444444444, %r9 + cmp %r9, %r14 + je .Lp_r15 + or $8, %r8d +.Lp_r15: + movabs $0x5555555555555555, %r9 + cmp %r9, %r15 + je .Lp_rbp + or $16, %r8d +.Lp_rbp: + movabs $0x6666666666666666, %r9 + cmp %r9, %rbp + je .Lp_sp + or $32, %r8d +.Lp_sp: + mov cepis__probe_sp(%rip), %r9 + cmp %r9, %rsp + je .Lp_df + or $64, %r8d + mov %r9, %rsp +.Lp_df: + pushfq + pop %r9 + test $0x400, %r9d + jz .Lp_done + or $128, %r8d + cld +.Lp_done: + mov cepis__probe_bp(%rip), %rbp + mov %r8d, %eax + lea -40(%rbp), %rsp + pop %r15 + pop %r14 + pop %r13 + pop %r12 + pop %rbx + pop %rbp + ret + .size cepis_abi_probe, . - cepis_abi_probe + + .data + .p2align 3 +cepis__probe_bp: + .quad 0 +cepis__probe_sp: + .quad 0 + + .section .note.GNU-stack,"",@progbits diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..52e50d2 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Bring the local API stand-in up, run the driver against it, tear it down. +# Exits non-zero when anything fails. $1 is the driver, the rest is a prefix +# to run it under (valgrind, gdb, ...). + +set -eu + +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +driver=$1 +shift + +fifo=$(mktemp -u) +mkfifo "$fifo" + +python3 "$here/server.py" 0 >"$fifo" & +server=$! + +cleanup() { + kill "$server" 2>/dev/null || true + wait "$server" 2>/dev/null || true + rm -f "$fifo" +} +trap cleanup EXIT INT TERM + +port=$(head -n 1 "$fifo") +if [ -z "$port" ]; then + echo "server did not start" >&2 + exit 1 +fi + +echo "server on 127.0.0.1:$port" +"$@" "$driver" "http://127.0.0.1:$port" diff --git a/tests/server.py b/tests/server.py new file mode 100755 index 0000000..c77d7f6 --- /dev/null +++ b/tests/server.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Local stand-in for the cep.is API, used by the test driver. + +Behaviour is selected by the first path segment, so a test points the client +at a different ``base_url`` to get a different server: ``/fail2`` fails twice +then answers, ``/always503`` never answers, and so on. Every prefix counts the +requests it saw; ``/_stats/<prefix>/health`` reports that count in the ``ceps`` +field so the C driver can assert on it without speaking HTTP itself. +""" + +import json +import sys +import threading +from datetime import datetime, timedelta, timezone +from email.utils import format_datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlparse + +COLUMNS = [ + "cep", "logradouro", "complemento", "unidade", "bairro", "localidade", + "uf", "estado", "regiao", "ibge", "gia", "ddd", "siafi", +] + +DB = { + "01001000": { + "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", + "geo": {"lat": -23.567202, "lon": -46.649659, "raio_m": 1084, + "precisao": "logradouro"}, + }, + "20040020": { + "cep": "20040020", "logradouro": "Avenida Rio Branco", + "complemento": "", "unidade": "", "bairro": "Centro", + "localidade": "Rio de Janeiro", "uf": "RJ", + "estado": "Rio de Janeiro", "regiao": "Sudeste", "ibge": "3304557", + "gia": "", "ddd": "21", "siafi": "6001", + }, + "69900000": { + "cep": "69900000", "logradouro": "", "complemento": "", "unidade": "", + "bairro": "", "localidade": "Rio Branco", "uf": "AC", + "estado": "Acre", "regiao": "Norte", "ibge": "1200401", "gia": "", + "ddd": "68", "siafi": "0605", + "geo": {"lat": -9.97499, "lon": -67.8243, "raio_m": 18420, + "precisao": "localidade"}, + }, + "12345678": { + "cep": "12345678", "logradouro": 'Rua A, B "quoted"', + "complemento": "até 100", "unidade": "", "bairro": "Vila, Nova", + "localidade": "Campinas", "uf": "SP", "estado": "São Paulo", + "regiao": "Sudeste", "ibge": "3509502", "gia": "2446", "ddd": "19", + "siafi": "6291", + }, +} + +for _n in range(600): + _c = "%08d" % (70000000 + _n) + DB[_c] = { + "cep": _c, "logradouro": "Rua %d" % _n, "complemento": "", + "unidade": "", "bairro": "Centro", "localidade": "Cidade %d" % _n, + "uf": "SP", "estado": "São Paulo", "regiao": "Sudeste", + "ibge": "3550308", "gia": "", "ddd": "11", "siafi": "7107", + } + +HITS = {} +LOCK = threading.Lock() + + +def bump(prefix): + with LOCK: + HITS[prefix] = HITS.get(prefix, 0) + 1 + return HITS[prefix] + + +def csv_escape(value): + if any(c in value for c in ',"\r\n'): + return '"' + value.replace('"', '""') + '"' + return value + + +def csv_row(cep): + entry = DB.get(cep) + if entry is None: + return ",".join([cep] + [""] * (len(COLUMNS) - 1)) + return ",".join(csv_escape(entry.get(c, "")) for c in COLUMNS) + + +class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, fmt, *args): + pass + + def send(self, code, body, ctype="application/json; charset=utf-8", + headers=None): + raw = body.encode("utf-8") if isinstance(body, str) else body + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(raw))) + for key, value in (headers or {}).items(): + self.send_header(key, value) + self.end_headers() + self.wfile.write(raw) + + def do_GET(self): + parsed = urlparse(self.path) + parts = [p for p in parsed.path.split("/") if p] + + if parts and parts[0] == "_stats": + name = parts[1] if len(parts) > 1 else "" + with LOCK: + count = HITS.get(name, 0) + self.send(200, json.dumps({ + "status": "ok", "dne": "test", "ceps": count, "geo": 0, + "base_gerada_em": "2026-07-24T03:48:28Z", + })) + return + + prefix = "" + if parts and not parts[0].isdigit() and parts[0] not in ("csv", "health"): + prefix = parts.pop(0) + count = bump(prefix) + + if prefix == "always503": + self.send(503, '{"erro":"interno","mensagem":"falha interna"}') + return + if prefix == "always429": + self.send(429, '{"erro":"limite","mensagem":"devagar"}', + headers={"Retry-After": "1"}) + return + if prefix == "date429": + when = datetime.now(timezone.utc) + timedelta(seconds=2) + self.send(429, '{"erro":"limite","mensagem":"devagar"}', + headers={"Retry-After": format_datetime(when, + usegmt=True)}) + return + if prefix == "hugeretry": + self.send(429, '{"erro":"limite","mensagem":"devagar"}', + headers={"Retry-After": "3600"}) + return + if prefix == "fail2" and count <= 2: + self.send(503, '{"erro":"interno","mensagem":"falha interna"}') + return + if prefix == "flaky" and count == 1: + self.send(500, '{"erro":"interno","mensagem":"falha interna"}') + return + if prefix == "always400": + self.send(400, '{"erro":"cep_invalido",' + '"mensagem":"CEP deve ter 8 dígitos"}') + return + if prefix == "badjson": + self.send(200, "<html>captive portal</html>", ctype="text/html") + return + if prefix == "badgeo": + entry = dict(DB["01001000"]) + entry["geo"] = {"lat": "nao-e-numero", "precisao": "logradouro"} + entry["campo_novo"] = {"nested": [1, 2, 3]} + self.send(200, json.dumps(entry, ensure_ascii=False)) + return + if prefix == "surrogate": + entry = dict(DB["01001000"]) + entry["logradouro"] = "Rua \U0001F600 Feliz" + self.send(200, json.dumps(entry, ensure_ascii=True)) + return + if prefix == "escaped": + self.send(200, json.dumps(DB["01001000"], ensure_ascii=True)) + return + + if parts and parts[0] == "health": + body = {"status": "ok", "dne": "26071", "ceps": 1596168, + "geo": 894489, "base_gerada_em": "2026-07-24T03:48:28Z"} + if prefix == "degraded": + body["status"] = "degraded" + self.send(503, json.dumps(body)) + return + self.send(200, json.dumps(body)) + return + + if parts and parts[0] == "csv": + query = parse_qs(parsed.query) + ceps = [c for c in query.get("ceps", [""])[0].split(",") if c] + if len(ceps) > 500: + self.send(400, '{"erro":"limite","mensagem":"máximo 500"}') + return + if prefix == "short": + ceps = ceps[:1] + rows = [",".join(COLUMNS)] + [csv_row(c) for c in ceps] + body = "\r\n".join(rows) + "\r\n" + if prefix == "bom": + body = "\ufeff" + body + self.send(200, body, ctype="text/csv; charset=utf-8") + return + + if not parts: + self.send(404, '{"erro":"cep_nao_encontrado",' + '"mensagem":"CEP não existe na base do DNE"}') + return + + cep = parts[0] + if not (len(cep) == 8 and cep.isdigit()): + self.send(400, '{"erro":"cep_invalido",' + '"mensagem":"CEP deve ter 8 dígitos"}') + return + entry = DB.get(cep) + if entry is None: + self.send(404, '{"erro":"cep_nao_encontrado",' + '"mensagem":"CEP não existe na base do DNE"}') + return + self.send(200, json.dumps(entry, ensure_ascii=False)) + + +def main(): + port = int(sys.argv[1]) if len(sys.argv) > 1 else 0 + server = ThreadingHTTPServer(("127.0.0.1", port), Handler) + print(server.server_address[1], flush=True) + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/tests/test_cepis.c b/tests/test_cepis.c new file mode 100644 index 0000000..3dd6dc6 --- /dev/null +++ b/tests/test_cepis.c @@ -0,0 +1,721 @@ +/* + * Test driver. Every case runs against the local server in tests/server.py, + * reached through base_url; nothing here mocks the HTTP layer. + * + * Usage: test_cepis http://127.0.0.1:<port> + */ + +#define _POSIX_C_SOURCE 200809L + +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include "cepis.h" + +_Static_assert(sizeof(cepis_geo) == 32, "cepis_geo layout"); +_Static_assert(offsetof(cepis_geo, precisao) == 24, "cepis_geo layout"); +_Static_assert(sizeof(cepis_address) == 120, "cepis_address layout"); +_Static_assert(offsetof(cepis_address, siafi) == 96, "cepis_address layout"); +_Static_assert(offsetof(cepis_address, geo) == 104, "cepis_address layout"); +_Static_assert(offsetof(cepis_address, raw) == 112, "cepis_address layout"); +_Static_assert(sizeof(struct cepis_health) == 48, "cepis_health layout"); +_Static_assert(offsetof(struct cepis_health, base_gerada_em) == 32, + "cepis_health layout"); +_Static_assert(sizeof(cepis_error) == 272, "cepis_error layout"); +_Static_assert(offsetof(cepis_error, retry_after) == 8, "cepis_error layout"); +_Static_assert(offsetof(cepis_error, message) == 16, "cepis_error layout"); +_Static_assert(sizeof(cepis_config) == 56, "cepis_config layout"); +_Static_assert(offsetof(cepis_config, max_attempts) == 16, + "cepis_config layout"); +_Static_assert(offsetof(cepis_config, user_agent) == 48, "cepis_config layout"); + +static const char *base; +static int failures; +static int checks; +static const char *current; + +#define CHECK(cond) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, " FAIL %s:%d [%s]: %s\n", __FILE__, __LINE__, \ + current, #cond); \ + } \ + } while (0) + +#define CHECK_STR(got, want) \ + do { \ + checks++; \ + const char *g_ = (got); \ + if (g_ == NULL || strcmp(g_, (want)) != 0) { \ + failures++; \ + fprintf(stderr, " FAIL %s:%d [%s]: want \"%s\" got \"%s\"\n", \ + __FILE__, __LINE__, current, (want), \ + g_ ? g_ : "(null)"); \ + } \ + } while (0) + +static void begin(const char *name) +{ + current = name; + printf("- %s\n", name); +} + +static char *urlf(const char *fmt, ...) +{ + static char buf[8][512]; + static int slot; + char *out = buf[slot++ % 8]; + va_list ap; + va_start(ap, fmt); + vsnprintf(out, 512, fmt, ap); + va_end(ap); + return out; +} + +static cepis_client *client_at(const char *url, int attempts, double backoff) +{ + cepis_config cfg; + cepis_client *client = NULL; + + memset(&cfg, 0, sizeof cfg); + cfg.base_url = url; + cfg.max_attempts = attempts; + cfg.backoff_base = backoff; + cfg.backoff_max = backoff * 4.0; + cfg.timeout = 5.0; + if (cepis_client_new(&cfg, &client) != CEPIS_OK) + return NULL; + return client; +} + +static long hits(const char *prefix) +{ + cepis_client *client; + + client = client_at(urlf("%s/_stats/%s", base, prefix), 1, 0.001); + struct cepis_health *health = NULL; + long count = -1; + + if (client == NULL) + return -1; + if (cepis_health(client, &health, NULL) == CEPIS_OK && health != NULL) + count = (long)health->ceps; + cepis_health_free(health); + cepis_client_free(client); + return count; +} + +static double now_seconds(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +} + +static void test_normalize(void) +{ + char out[16]; + + begin("normalize_cep"); + CHECK(cepis_normalize_cep("01001-000", out) == CEPIS_OK); + CHECK_STR(out, "01001000"); + CHECK(cepis_normalize_cep("01001000", out) == CEPIS_OK); + CHECK_STR(out, "01001000"); + CHECK(cepis_normalize_cep(" 01001-000 ", out) == CEPIS_OK); + CHECK_STR(out, "01001000"); + CHECK(cepis_normalize_cep("01.001-000", out) == CEPIS_OK); + CHECK_STR(out, "01001000"); + CHECK(cepis_normalize_cep("0100100", out) == CEPIS_ERR_INVALID_CEP); + CHECK_STR(out, ""); + CHECK(cepis_normalize_cep("010010000", out) == CEPIS_ERR_INVALID_CEP); + CHECK(cepis_normalize_cep("0100100a", out) == CEPIS_ERR_INVALID_CEP); + CHECK(cepis_normalize_cep("", out) == CEPIS_ERR_INVALID_CEP); + CHECK(cepis_normalize_cep(NULL, out) == CEPIS_ERR_INVALID_CEP); + CHECK(cepis_normalize_cep("01001000", NULL) == CEPIS_ERR_USAGE); +} + +static void test_lookup_geo(void) +{ + cepis_client *client = client_at(base, 3, 0.001); + cepis_address *address = NULL; + cepis_error err; + char masked[16]; + + begin("lookup with geo"); + CHECK(cepis_lookup(client, "01001-000", &address, &err) == CEPIS_OK); + if (address != NULL) { + CHECK_STR(address->cep, "01001000"); + CHECK_STR(address->logradouro, "Praça da Sé"); + CHECK_STR(address->complemento, "lado ímpar"); + CHECK_STR(address->unidade, ""); + CHECK_STR(address->bairro, "Sé"); + CHECK_STR(address->localidade, "São Paulo"); + CHECK_STR(address->uf, "SP"); + CHECK_STR(address->estado, "São Paulo"); + CHECK_STR(address->regiao, "Sudeste"); + CHECK_STR(address->ibge, "3550308"); + CHECK_STR(address->gia, "1004"); + CHECK_STR(address->ddd, "11"); + CHECK_STR(address->siafi, "7107"); + CHECK(address->raw != NULL); + CHECK(strstr(address->raw, "logradouro") != NULL); + CHECK(address->geo != NULL); + if (address->geo != NULL) { + CHECK(address->geo->lat < -23.5 && address->geo->lat > -23.6); + CHECK(address->geo->lon < -46.6 && address->geo->lon > -46.7); + CHECK(address->geo->raio_m == 1084); + CHECK_STR(address->geo->precisao, "logradouro"); + CHECK(cepis_geo_is_precise(address->geo) == 1); + } + cepis_address_formatted_cep(address, masked); + CHECK_STR(masked, "01001-000"); + } + cepis_address_free(address); + + address = NULL; + CHECK(cepis_lookup(client, "69900000", &address, &err) == CEPIS_OK); + if (address != NULL) { + CHECK(address->geo != NULL); + if (address->geo != NULL) { + CHECK_STR(address->geo->precisao, "localidade"); + CHECK(cepis_geo_is_precise(address->geo) == 0); + CHECK(address->geo->raio_m == 18420); + } + } + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_lookup_no_geo(void) +{ + cepis_client *client = client_at(base, 3, 0.001); + cepis_address *address = NULL; + + begin("lookup without geo"); + CHECK(cepis_lookup(client, "20040020", &address, NULL) == CEPIS_OK); + if (address != NULL) { + CHECK(address->geo == NULL); + CHECK_STR(address->localidade, "Rio de Janeiro"); + CHECK_STR(address->gia, ""); + } + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_escaped_json(void) +{ + cepis_client *client = client_at(urlf("%s/escaped", base), 1, 0.001); + cepis_address *address = NULL; + + begin("\\u escapes decoded to UTF-8"); + CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK); + if (address != NULL) { + CHECK_STR(address->logradouro, "Praça da Sé"); + CHECK_STR(address->localidade, "São Paulo"); + } + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_surrogate_pair(void) +{ + cepis_client *client = client_at(urlf("%s/surrogate", base), 1, 0.001); + cepis_address *address = NULL; + + begin("a surrogate pair decodes to one UTF-8 sequence"); + CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK); + if (address != NULL) + CHECK_STR(address->logradouro, "Rua \xf0\x9f\x98\x80 Feliz"); + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_csv_bom(void) +{ + const char *ceps[] = {"01001000", "20040020"}; + cepis_client *client = client_at(urlf("%s/bom", base), 1, 0.001); + cepis_address **out = NULL; + + begin("a UTF-8 BOM on the CSV is stripped"); + CHECK(cepis_lookup_many(client, ceps, 2, &out, NULL) == CEPIS_OK); + if (out != NULL) { + CHECK(out[0] != NULL); + CHECK(out[1] != NULL); + if (out[0] != NULL) + CHECK_STR(out[0]->cep, "01001000"); + } + cepis_address_list_free(out, 2); + cepis_client_free(client); +} + +static void test_not_found_not_retried(void) +{ + cepis_client *client = client_at(urlf("%s/nf", base), 3, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("404 is not retried"); + CHECK(cepis_lookup(client, "99999999", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(address == NULL); + CHECK(err.status == CEPIS_ERR_NOT_FOUND); + CHECK(strstr(err.message, "99999999") != NULL); + CHECK(hits("nf") == 1); + cepis_client_free(client); +} + +static void test_bad_cep_from_api(void) +{ + cepis_client *client = client_at(urlf("%s/always400", base), 3, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("400 is not retried and carries the API message"); + CHECK(cepis_lookup(client, "00000000", &address, &err) == + CEPIS_ERR_INVALID_CEP); + CHECK(address == NULL); + CHECK(strstr(err.message, "8 d") != NULL); + CHECK(hits("always400") == 1); + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_local_validation_makes_no_request(void) +{ + cepis_client *client = client_at(urlf("%s/novalid", base), 3, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("local validation makes no request"); + CHECK(cepis_lookup(client, "123", &address, &err) == CEPIS_ERR_INVALID_CEP); + CHECK(address == NULL); + CHECK(strstr(err.message, "8 digits") != NULL); + CHECK(hits("novalid") == 0); + cepis_client_free(client); +} + +static void test_retry_then_success(void) +{ + cepis_client *client = client_at(urlf("%s/fail2", base), 3, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("retry then success"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_OK); + CHECK(address != NULL); + CHECK(hits("fail2") == 3); + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_retry_exhausted(void) +{ + cepis_client *client = client_at(urlf("%s/always503", base), 2, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("retry exhausted on 5xx"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER); + CHECK(address == NULL); + CHECK(err.status == CEPIS_ERR_SERVER); + CHECK(err.http_status == 503); + CHECK(strstr(err.message, "503") != NULL); + CHECK(hits("always503") == 2); + cepis_client_free(client); +} + +static void test_rate_limited(void) +{ + cepis_client *client = client_at(urlf("%s/always429", base), 2, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("429 exposes Retry-After in seconds"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == + CEPIS_ERR_RATE_LIMITED); + CHECK(address == NULL); + CHECK(err.http_status == 429); + CHECK(err.retry_after > 0.5 && err.retry_after < 1.5); + CHECK(hits("always429") == 2); + cepis_client_free(client); +} + +static void test_rate_limited_http_date(void) +{ + cepis_client *client = client_at(urlf("%s/date429", base), 2, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("429 with an HTTP-date Retry-After"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == + CEPIS_ERR_RATE_LIMITED); + CHECK(err.retry_after > 0.0 && err.retry_after <= 3.0); + CHECK(hits("date429") == 2); + cepis_client_free(client); +} + +static void test_retry_after_is_capped(void) +{ + cepis_client *client = client_at(urlf("%s/hugeretry", base), 2, 0.01); + cepis_address *address = NULL; + cepis_error err; + double started = now_seconds(); + + begin("a huge Retry-After is capped at backoff_max"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == + CEPIS_ERR_RATE_LIMITED); + CHECK(now_seconds() - started < 5.0); + CHECK(err.retry_after > 3000.0); + cepis_client_free(client); +} + +static void test_total_timeout(void) +{ + cepis_config cfg; + cepis_client *client = NULL; + cepis_address *address = NULL; + cepis_error err; + double started; + + begin("total_timeout bounds the whole call"); + memset(&cfg, 0, sizeof cfg); + cfg.base_url = urlf("%s/always503", base); + cfg.max_attempts = 50; + cfg.backoff_base = 0.05; + cfg.backoff_max = 0.05; + cfg.total_timeout = 0.4; + CHECK(cepis_client_new(&cfg, &client) == CEPIS_OK); + started = now_seconds(); + CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER); + CHECK(now_seconds() - started < 4.0); + cepis_client_free(client); +} + +static void test_invalid_response(void) +{ + cepis_client *client = client_at(urlf("%s/badjson", base), 1, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("a 200 with an unparseable body"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == + CEPIS_ERR_INVALID_RESPONSE); + CHECK(address == NULL); + cepis_client_free(client); +} + +static void test_bad_geo_kept_address(void) +{ + cepis_client *client = client_at(urlf("%s/badgeo", base), 1, 0.001); + cepis_address *address = NULL; + + begin("a malformed geo drops the geo, not the address"); + CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK); + if (address != NULL) { + CHECK(address->geo == NULL); + CHECK_STR(address->localidade, "São Paulo"); + CHECK(strstr(address->raw, "campo_novo") != NULL); + } + cepis_address_free(address); + cepis_client_free(client); +} + +static void test_network_error(void) +{ + cepis_client *client = client_at("http://127.0.0.1:1", 2, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("a refused connection is a network error"); + CHECK(cepis_lookup(client, "01001000", &address, &err) == + CEPIS_ERR_NETWORK); + CHECK(address == NULL); + CHECK(err.message[0] != '\0'); + cepis_client_free(client); +} + +static void test_health(void) +{ + cepis_client *client = client_at(base, 2, 0.001); + struct cepis_health *health = NULL; + + begin("health"); + CHECK(cepis_health(client, &health, NULL) == CEPIS_OK); + if (health != NULL) { + CHECK_STR(health->status, "ok"); + CHECK_STR(health->dne, "26071"); + CHECK(health->ceps == 1596168); + CHECK(health->geo == 894489); + CHECK_STR(health->base_gerada_em, "2026-07-24T03:48:28Z"); + CHECK(health->raw != NULL); + } + cepis_health_free(health); + cepis_client_free(client); +} + +static void test_health_degraded(void) +{ + cepis_client *client = client_at(urlf("%s/degraded", base), 2, 0.001); + struct cepis_health *health = NULL; + + begin("a degraded API is not an error"); + CHECK(cepis_health(client, &health, NULL) == CEPIS_OK); + if (health != NULL) + CHECK_STR(health->status, "degraded"); + CHECK(hits("degraded") == 1); + cepis_health_free(health); + cepis_client_free(client); +} + +static void test_batch_order(void) +{ + const char *ceps[] = {"01001000", "99999999", "20040020"}; + cepis_client *client = client_at(urlf("%s/batch", base), 2, 0.001); + cepis_address **out = NULL; + + begin("batch keeps order and nulls the missing entry"); + CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK); + if (out != NULL) { + CHECK(out[0] != NULL); + CHECK(out[1] == NULL); + CHECK(out[2] != NULL); + if (out[0] != NULL) { + CHECK_STR(out[0]->localidade, "São Paulo"); + CHECK(out[0]->geo == NULL); + CHECK(out[0]->raw == NULL); + } + if (out[2] != NULL) + CHECK_STR(out[2]->localidade, "Rio de Janeiro"); + } + CHECK(hits("batch") == 1); + cepis_address_list_free(out, 3); + cepis_client_free(client); +} + +static void test_batch_quoting(void) +{ + const char *ceps[] = {"12345678"}; + cepis_client *client = client_at(base, 2, 0.001); + cepis_address **out = NULL; + + begin("batch parses quoted CSV fields"); + CHECK(cepis_lookup_many(client, ceps, 1, &out, NULL) == CEPIS_OK); + if (out != NULL && out[0] != NULL) { + CHECK_STR(out[0]->logradouro, "Rua A, B \"quoted\""); + CHECK_STR(out[0]->bairro, "Vila, Nova"); + CHECK_STR(out[0]->complemento, "até 100"); + } + cepis_address_list_free(out, 1); + cepis_client_free(client); +} + +static void test_batch_malformed_entry(void) +{ + const char *ceps[] = {"01001000", "abc", "20040020"}; + cepis_client *client = client_at(base, 2, 0.001); + cepis_address **out = NULL; + + begin("a malformed CEP in a batch is null, not an error"); + CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK); + if (out != NULL) { + CHECK(out[0] != NULL); + CHECK(out[1] == NULL); + CHECK(out[2] != NULL); + } + cepis_address_list_free(out, 3); + cepis_client_free(client); +} + +static void test_batch_chunking(void) +{ + cepis_client *client = client_at(urlf("%s/chunk", base), 2, 0.001); + cepis_address **out = NULL; + const char **ceps; + char *storage; + size_t i; + size_t n = 501; + int ok = 1; + + begin("batches above 500 are chunked"); + ceps = malloc(n * sizeof *ceps); + storage = malloc(n * 9); + for (i = 0; i < n; i++) { + snprintf(storage + i * 9, 9, "%08zu", (size_t)70000000 + i); + ceps[i] = storage + i * 9; + } + CHECK(cepis_lookup_many(client, ceps, n, &out, NULL) == CEPIS_OK); + if (out != NULL) { + for (i = 0; i < n; i++) { + if (out[i] == NULL || strcmp(out[i]->cep, ceps[i]) != 0) { + ok = 0; + break; + } + } + CHECK(ok); + } + CHECK(hits("chunk") == 2); + cepis_address_list_free(out, n); + free(storage); + free(ceps); + cepis_client_free(client); +} + +static void test_batch_short_response(void) +{ + const char *ceps[] = {"01001000", "20040020", "12345678"}; + cepis_client *client = client_at(urlf("%s/short", base), 2, 0.001); + cepis_address **out = NULL; + + begin("a short batch response is padded, not shifted"); + CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK); + if (out != NULL) { + CHECK(out[0] != NULL); + CHECK(out[1] == NULL); + CHECK(out[2] == NULL); + if (out[0] != NULL) + CHECK_STR(out[0]->cep, "01001000"); + } + cepis_address_list_free(out, 3); + cepis_client_free(client); +} + +static void test_batch_empty(void) +{ + cepis_client *client = client_at(base, 2, 0.001); + cepis_address **out = NULL; + + begin("an empty batch is a valid empty result"); + CHECK(cepis_lookup_many(client, NULL, 0, &out, NULL) == CEPIS_OK); + CHECK(out != NULL); + cepis_address_list_free(out, 0); + cepis_client_free(client); +} + +static void test_trailing_slash(void) +{ + cepis_client *client = client_at(urlf("%s/", base), 2, 0.001); + cepis_address *address = NULL; + + begin("a trailing slash in base_url is stripped"); + CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK); + CHECK(address != NULL); + cepis_address_free(address); + cepis_client_free(client); +} + +int cepis_abi_probe(void *fn, void *a0, void *a1, void *a2, void *a3); + +static void probe(const char *name, void *fn, void *a0, void *a1, void *a2, + void *a3) +{ + int mask = cepis_abi_probe(fn, a0, a1, a2, a3); + + checks++; + if (mask != 0) { + failures++; + fprintf(stderr, " FAIL [%s]: %s violated the ABI, mask 0x%02x\n", + current, name, mask); + } +} + +static void test_abi(void) +{ + cepis_client *client = client_at(base, 2, 0.001); + cepis_address *address = NULL; + struct cepis_health *health = NULL; + cepis_address **many = NULL; + cepis_error err; + char out[16]; + + begin("callee-saved registers, stack pointer and DF survive every call"); + probe("cepis_normalize_cep", (void *)cepis_normalize_cep, + (void *)"01001-000", out, NULL, NULL); + probe("cepis_strerror", (void *)cepis_strerror, (void *)(long)4, NULL, + NULL, NULL); + probe("cepis_lookup(invalid)", (void *)cepis_lookup, client, (void *)"1", + &address, &err); + probe("cepis_lookup", (void *)cepis_lookup, client, (void *)"01001000", + &address, &err); + probe("cepis_address_free", (void *)cepis_address_free, address, NULL, + NULL, NULL); + probe("cepis_health", (void *)cepis_health, client, &health, &err, NULL); + probe("cepis_health_free", (void *)cepis_health_free, health, NULL, NULL, + NULL); + probe("cepis_lookup_many", (void *)cepis_lookup_many, client, NULL, NULL, + &many); + cepis_address_list_free(many, 0); + probe("cepis_default_client", (void *)cepis_default_client, NULL, NULL, + NULL, NULL); + probe("cepis_client_free", (void *)cepis_client_free, client, NULL, NULL, + NULL); +} + +static void test_usage(void) +{ + cepis_client *client = client_at(base, 2, 0.001); + cepis_config cfg; + cepis_client *bad = NULL; + + begin("argument errors"); + CHECK(cepis_lookup(client, "01001000", NULL, NULL) == CEPIS_ERR_USAGE); + CHECK(cepis_health(client, NULL, NULL) == CEPIS_ERR_USAGE); + CHECK(cepis_lookup_many(client, NULL, 0, NULL, NULL) == CEPIS_ERR_USAGE); + memset(&cfg, 0, sizeof cfg); + cfg.max_attempts = -1; + CHECK(cepis_client_new(&cfg, &bad) == CEPIS_ERR_USAGE); + CHECK(bad == NULL); + CHECK(strcmp(cepis_version(), CEPIS_VERSION) == 0); + CHECK(strcmp(cepis_strerror(CEPIS_ERR_NOT_FOUND), "CEP not found") == 0); + cepis_client_free(client); +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "usage: %s <base-url>\n", argv[0]); + return 2; + } + base = argv[1]; + cepis_global_init(); + + test_normalize(); + test_lookup_geo(); + test_lookup_no_geo(); + test_escaped_json(); + test_surrogate_pair(); + test_csv_bom(); + test_not_found_not_retried(); + test_bad_cep_from_api(); + test_local_validation_makes_no_request(); + test_retry_then_success(); + test_retry_exhausted(); + test_rate_limited(); + test_rate_limited_http_date(); + test_retry_after_is_capped(); + test_total_timeout(); + test_invalid_response(); + test_bad_geo_kept_address(); + test_network_error(); + test_health(); + test_health_degraded(); + test_batch_order(); + test_batch_quoting(); + test_batch_malformed_entry(); + test_batch_chunking(); + test_batch_short_response(); + test_batch_empty(); + test_trailing_slash(); + test_abi(); + test_usage(); + + cepis_global_cleanup(); + + printf("\n%d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} |