/* * 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: */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #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 \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; }