#include "cepis.h" #include "test_server.h" #include #include #include #include #include #include static int tests_run; static int tests_failed; static const char *current_test; static bool current_failed; #define CHECK(expr) \ do { \ if (!(expr)) { \ printf(" FAIL %s:%d: %s\n", __FILE__, __LINE__, \ #expr); \ current_failed = true; \ } \ } while (0) #define CHECK_STR(actual, expected) \ do { \ const char *a_ = (actual); \ const char *e_ = (expected); \ if (a_ == NULL || strcmp(a_, e_) != 0) { \ printf(" FAIL %s:%d: expected \"%s\", got \"%s\"\n", \ __FILE__, __LINE__, e_, \ a_ != NULL ? a_ : "(null)"); \ current_failed = true; \ } \ } while (0) #define RUN(fn) \ do { \ current_test = #fn; \ current_failed = false; \ tests_run++; \ printf("%s\n", current_test); \ fn(); \ if (current_failed) \ tests_failed++; \ } while (0) static const char SE_ADDRESS[] = "{\"cep\":\"01001000\",\"logradouro\":\"Pra\xC3\xA7\x61 da S\xC3\xA9\"," "\"complemento\":\"lado \xC3\xADmpar\",\"unidade\":\"\",\"bairro\":" "\"S\xC3\xA9\",\"localidade\":\"S\xC3\xA3o Paulo\",\"uf\":\"SP\"," "\"estado\":\"S\xC3\xA3o Paulo\",\"regiao\":\"Sudeste\",\"ibge\":" "\"3550308\",\"gia\":\"1004\",\"ddd\":\"11\",\"siafi\":\"7107\"}"; static const char CSV_HEADER[] = "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao," "ibge,gia,ddd,siafi\n"; typedef struct { test_server_t *server; cepis_client_t *client; } fixture_t; static void fixture_up(fixture_t *fixture) { cepis_config_t config; cepis_error_t err; fixture->server = ts_start(); if (fixture->server == NULL) { printf(" FAIL could not start the test server\n"); current_failed = true; fixture->client = NULL; return; } cepis_config_init(&config); config.base_url = ts_base_url(fixture->server); config.timeout = 5.0; config.backoff_base = 0.001; config.backoff_max = 0.01; if (cepis_client_new(&config, &fixture->client, &err) != CEPIS_OK) { printf(" FAIL could not build a client: %s\n", err.message); current_failed = true; fixture->client = NULL; } } static void fixture_down(fixture_t *fixture) { cepis_client_free(fixture->client); ts_stop(fixture->server); } static double elapsed_since(double start) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ((double)ts.tv_sec + (double)ts.tv_nsec / 1e9) - start; } static double now_seconds(void) { return elapsed_since(0.0); } static void test_normalize_accepts_masked_and_unmasked(void) { static const char *inputs[] = {"01001000", "01001-000", " 01001-000 ", "01001.000"}; char out[CEPIS_CEP_LEN + 1]; size_t i; for (i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) { CHECK(cepis_normalize_cep(inputs[i], out) == CEPIS_OK); CHECK_STR(out, "01001000"); } } static void test_normalize_rejects_bad_format(void) { static const char *inputs[] = {"", "123", "0100100a", "010010000", "abcdefgh", "-1001000"}; char out[CEPIS_CEP_LEN + 1]; size_t i; for (i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) CHECK(cepis_normalize_cep(inputs[i], out) == CEPIS_ERR_INVALID_CEP); CHECK(cepis_normalize_cep(NULL, out) == CEPIS_ERR_INVALID_CEP); } static void test_format_cep(void) { char out[CEPIS_CEP_LEN + 2]; CHECK(cepis_format_cep("01001000", out) == CEPIS_OK); CHECK_STR(out, "01001-000"); CHECK(cepis_format_cep("123", out) == CEPIS_ERR_INVALID_CEP); } static void test_successful_lookup(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; char masked[CEPIS_CEP_LEN + 2]; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 200, SE_ADDRESS, NULL); CHECK(cepis_lookup(fixture.client, "01001-000", &address, &err) == CEPIS_OK); if (address != NULL) { CHECK_STR(address->localidade, "S\xC3\xA3o Paulo"); CHECK_STR(address->logradouro, "Pra\xC3\xA7\x61 da S\xC3\xA9"); CHECK_STR(address->uf, "SP"); CHECK_STR(address->cep, "01001000"); CHECK(address->geo == NULL); CHECK(address->raw != NULL); CHECK(cepis_format_cep(address->cep, masked) == CEPIS_OK); CHECK_STR(masked, "01001-000"); } cepis_address_free(address); fixture_down(&fixture); } static void test_escaped_and_utf8_strings_decode(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 200, "{\"cep\":\"01001000\",\"logradouro\":" "\"Pra\\u00e7a da S\\u00e9\",\"localidade\":" "\"S\\u00e3o Paulo\",\"bairro\":\"a\\\"b\\\\c\\td\"," "\"uf\":\"SP\"}", NULL); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_OK); if (address != NULL) { CHECK_STR(address->logradouro, "Pra\xC3\xA7\x61 da S\xC3\xA9"); CHECK_STR(address->localidade, "S\xC3\xA3o Paulo"); CHECK_STR(address->bairro, "a\"b\\c\td"); CHECK_STR(address->complemento, ""); } cepis_address_free(address); fixture_down(&fixture); } static void test_reads_the_coordinate_when_present(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01310100", 200, "{\"cep\":\"01310100\",\"localidade\":\"S\xC3\xA3o Paulo\"," "\"geo\":{\"lat\":-23.567202,\"lon\":-46.649659," "\"raio_m\":120,\"precisao\":\"logradouro\"}}", NULL); CHECK(cepis_lookup(fixture.client, "01310100", &address, &err) == CEPIS_OK); if (address != NULL && address->geo != NULL) { CHECK(fabs(address->geo->lat + 23.567202) < 1e-9); CHECK(fabs(address->geo->lon + 46.649659) < 1e-9); CHECK(address->geo->raio_m == 120); CHECK_STR(address->geo->precisao, "logradouro"); CHECK(cepis_geo_is_precise(address->geo)); } else { CHECK(address != NULL && address->geo != NULL); } cepis_address_free(address); fixture_down(&fixture); } static void test_municipality_coordinate_is_not_precise(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 200, "{\"cep\":\"01001000\",\"localidade\":\"Manaus\"," "\"geo\":{\"lat\":-3.1,\"lon\":-60.0,\"raio_m\":22000," "\"precisao\":\"localidade\"}}", NULL); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_OK); CHECK(address != NULL && address->geo != NULL); if (address != NULL) { CHECK(!cepis_geo_is_precise(address->geo)); if (address->geo != NULL) CHECK(address->geo->raio_m == 22000); } cepis_address_free(address); fixture_down(&fixture); } static void test_malformed_geo_does_not_lose_the_address(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 200, "{\"cep\":\"01001000\",\"localidade\":\"S\xC3\xA3o Paulo\"," "\"geo\":{\"lat\":\"not a number\"}}", NULL); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_OK); if (address != NULL) { CHECK(address->geo == NULL); CHECK_STR(address->localidade, "S\xC3\xA3o Paulo"); } cepis_address_free(address); fixture_down(&fixture); } static void test_new_api_field_does_not_break(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 200, "{\"cep\":\"01001000\",\"localidade\":\"Santos\"," "\"campo_do_futuro\":\"value\"}", NULL); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_OK); if (address != NULL) { CHECK_STR(address->localidade, "Santos"); CHECK(address->raw != NULL && strstr(address->raw, "campo_do_futuro") != NULL); } cepis_address_free(address); fixture_down(&fixture); } static void test_not_found_is_not_retried(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/99999999", 404, "{\"erro\":\"cep_nao_encontrado\",\"mensagem\":" "\"CEP n\xC3\xA3o existe na base do DNE\"}", NULL); CHECK(cepis_lookup(fixture.client, "99999999", &address, &err) == CEPIS_ERR_NOT_FOUND); CHECK(address == NULL); CHECK(err.http_status == 404); CHECK(strstr(err.message, "99999999") != NULL); CHECK(ts_count(fixture.server, "/99999999") == 1); fixture_down(&fixture); } static void test_not_found_names_the_municipality(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; const cepis_faixa_t *faixa; fixture_up(&fixture); ts_respond(fixture.server, "/25845000", 404, "{\"erro\":\"cep_nao_encontrado\",\"mensagem\":" "\"CEP n\xC3\xA3o existe na base do DNE\",\"faixa\":" "{\"localidade\":\"Areal\",\"uf\":\"RJ\",\"estado\":" "\"Rio de Janeiro\",\"regiao\":\"Sudeste\",\"ibge\":" "\"3300225\",\"cep_ini\":\"25845001\",\"cep_fim\":" "\"25849899\",\"ceps\":118}}", NULL); /* Still a 404: the block is context for the failure, not an address. */ CHECK(cepis_lookup(fixture.client, "25845-000", &address, &err) == CEPIS_ERR_NOT_FOUND); CHECK(address == NULL); CHECK(err.http_status == 404); CHECK(err.has_faixa); faixa = cepis_error_faixa(&err); CHECK(faixa == &err.faixa); if (faixa != NULL) { CHECK_STR(faixa->localidade, "Areal"); CHECK_STR(faixa->uf, "RJ"); CHECK_STR(faixa->estado, "Rio de Janeiro"); CHECK_STR(faixa->regiao, "Sudeste"); CHECK_STR(faixa->ibge, "3300225"); /* * Not the DNE's own start, which is 25845000 -- the CEP that * just failed. These are the smallest and largest that really * exist, and the 118 are sparse across the ~4900 between them. */ CHECK_STR(faixa->cep_ini, "25845001"); CHECK_STR(faixa->cep_fim, "25849899"); CHECK(faixa->ceps == 118); } fixture_down(&fixture); } static void test_not_found_without_faixa_leaves_it_absent(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/99999999", 404, "{\"erro\":\"cep_nao_encontrado\",\"mensagem\":" "\"CEP n\xC3\xA3o existe na base do DNE\"}", NULL); /* A band no municipality owns: the API omits the block, never nulls * it, and the caller must be able to tell absent from present. */ CHECK(cepis_lookup(fixture.client, "99999999", &address, &err) == CEPIS_ERR_NOT_FOUND); CHECK(!err.has_faixa); CHECK(cepis_error_faixa(&err) == NULL); fixture_down(&fixture); } static void test_malformed_faixa_is_still_a_clean_not_found(void) { static const char *bodies[] = { "{\"erro\":\"x\",\"faixa\":null}", "{\"erro\":\"x\",\"faixa\":\"Areal\"}", "{\"erro\":\"x\",\"faixa\":[]}", "{\"erro\":\"x\",\"faixa\":{}}", "{\"erro\":\"x\",\"faixa\":{\"localidade\":\"\"}}", "{\"erro\":\"x\",\"faixa\":{\"uf\":\"RJ\",\"ceps\":118}}", "{\"erro\":\"x\",\"faixa\":{\"localidade\":42}}", "not json at all", ""}; fixture_t fixture; cepis_error_t err; size_t i; fixture_up(&fixture); for (i = 0; i < sizeof(bodies) / sizeof(bodies[0]); i++) { cepis_address_t *address = NULL; ts_respond(fixture.server, "/99999999", 404, bodies[i], NULL); CHECK(cepis_lookup(fixture.client, "99999999", &address, &err) == CEPIS_ERR_NOT_FOUND); CHECK(address == NULL); CHECK(!err.has_faixa); CHECK(cepis_error_faixa(&err) == NULL); } fixture_down(&fixture); } static void test_faixa_does_not_survive_the_next_error(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/25845000", 404, "{\"erro\":\"cep_nao_encontrado\",\"faixa\":" "{\"localidade\":\"Areal\",\"uf\":\"RJ\",\"cep_ini\":" "\"25845001\",\"cep_fim\":\"25849899\",\"ceps\":118}}", NULL); CHECK(cepis_lookup(fixture.client, "25845000", &address, &err) == CEPIS_ERR_NOT_FOUND); CHECK(err.has_faixa); /* Same error struct reused: a stale block would be read as a * municipality this second failure never named. */ CHECK(cepis_lookup(fixture.client, "123", &address, &err) == CEPIS_ERR_INVALID_CEP); CHECK(!err.has_faixa); CHECK(cepis_error_faixa(&err) == NULL); fixture_down(&fixture); } static void test_invalid_cep_never_leaves_the_machine(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); CHECK(cepis_lookup(fixture.client, "123", &address, &err) == CEPIS_ERR_INVALID_CEP); CHECK(address == NULL); CHECK(ts_total(fixture.server) == 0); fixture_down(&fixture); } static void test_api_400_becomes_invalid_cep(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/00000000", 400, "{\"erro\":\"cep_invalido\",\"mensagem\":" "\"rejected by the API\"}", NULL); CHECK(cepis_lookup(fixture.client, "00000000", &address, &err) == CEPIS_ERR_INVALID_CEP); CHECK(strstr(err.message, "rejected by the API") != NULL); CHECK(ts_count(fixture.server, "/00000000") == 1); fixture_down(&fixture); } static void test_response_that_is_not_json(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 200, "captive portal", NULL); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_ERR_INVALID_RESPONSE); CHECK(address == NULL); fixture_down(&fixture); } static void test_retries_on_500_then_succeeds(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; ts_reply_t replies[2]; fixture_up(&fixture); replies[0].status = 500; replies[0].body = "{\"erro\":\"interno\"}"; replies[0].headers = NULL; replies[1].status = 200; replies[1].body = SE_ADDRESS; replies[1].headers = NULL; ts_respond_seq(fixture.server, "/01001000", replies, 2); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_OK); if (address != NULL) CHECK_STR(address->localidade, "S\xC3\xA3o Paulo"); CHECK(ts_count(fixture.server, "/01001000") == 2); cepis_address_free(address); fixture_down(&fixture); } static void test_exhausts_attempts_then_fails(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 503, "{\"erro\":\"interno\"}", NULL); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_ERR_SERVER); CHECK(err.http_status == 503); CHECK(ts_count(fixture.server, "/01001000") == 3); fixture_down(&fixture); } static void test_max_attempts_one_disables_retry(void) { fixture_t fixture; cepis_client_t *client = NULL; cepis_address_t *address = NULL; cepis_config_t config; cepis_error_t err; fixture_up(&fixture); cepis_config_init(&config); config.base_url = ts_base_url(fixture.server); config.timeout = 5.0; config.max_attempts = 1; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_OK); ts_respond(fixture.server, "/01001000", 500, "{}", NULL); CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER); CHECK(ts_count(fixture.server, "/01001000") == 1); cepis_client_free(client); fixture_down(&fixture); } static void test_429_becomes_rate_limited_with_retry_after(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 429, "{}", "Retry-After: 7"); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_ERR_RATE_LIMITED); CHECK(fabs(err.retry_after - 7.0) < 1e-9); CHECK(err.http_status == 429); fixture_down(&fixture); } static void test_retry_after_http_date_is_understood(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; char header[128]; time_t target = time(NULL) + 120; struct tm parts; fixture_up(&fixture); gmtime_r(&target, &parts); strftime(header, sizeof(header), "Retry-After: %a, %d %b %Y %H:%M:%S GMT", &parts); ts_respond(fixture.server, "/01001000", 429, "{}", header); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_ERR_RATE_LIMITED); CHECK(err.retry_after > 100.0 && err.retry_after < 130.0); fixture_down(&fixture); } static void test_retry_after_honours_the_cap(void) { fixture_t fixture; cepis_address_t *address = NULL; cepis_error_t err; double started; fixture_up(&fixture); ts_respond(fixture.server, "/01001000", 429, "{}", "Retry-After: 3600"); started = now_seconds(); CHECK(cepis_lookup(fixture.client, "01001000", &address, &err) == CEPIS_ERR_RATE_LIMITED); CHECK(elapsed_since(started) < 5.0); fixture_down(&fixture); } static void test_network_error_when_nothing_listens(void) { cepis_client_t *client = NULL; cepis_address_t *address = NULL; cepis_config_t config; cepis_error_t err; cepis_config_init(&config); config.base_url = "http://127.0.0.1:1"; config.timeout = 1.0; config.max_attempts = 2; config.backoff_base = 0.001; config.backoff_max = 0.01; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_OK); CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_NETWORK); CHECK(address == NULL); cepis_client_free(client); } static void test_total_timeout_bounds_the_whole_call(void) { fixture_t fixture; cepis_client_t *client = NULL; cepis_address_t *address = NULL; cepis_config_t config; cepis_error_t err; double started; fixture_up(&fixture); cepis_config_init(&config); config.base_url = ts_base_url(fixture.server); config.timeout = 5.0; config.max_attempts = 50; config.backoff_base = 0.05; config.backoff_max = 0.05; config.total_timeout = 0.3; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_OK); ts_respond(fixture.server, "/01001000", 500, "{}", NULL); started = now_seconds(); CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER); CHECK(elapsed_since(started) < 3.0); CHECK(ts_count(fixture.server, "/01001000") < 50); cepis_client_free(client); fixture_down(&fixture); } static char *csv_body(const char *const *rows, size_t count) { size_t header_len = sizeof(CSV_HEADER) - 1; size_t total = header_len; size_t used; char *body; size_t i; for (i = 0; i < count; i++) total += strlen(rows[i]) + 1; body = malloc(total + 1); if (body == NULL) return NULL; memcpy(body, CSV_HEADER, header_len); used = header_len; for (i = 0; i < count; i++) { size_t len = strlen(rows[i]); memcpy(body + used, rows[i], len); used += len; body[used++] = '\n'; } body[used] = '\0'; return body; } static void test_batch_preserves_order_and_marks_missing(void) { static const char *rows[] = { "01001000,Pra\xC3\xA7\x61 da S\xC3\xA9,lado \xC3\xADmpar,," "S\xC3\xA9,S\xC3\xA3o Paulo,SP,S\xC3\xA3o " "Paulo,Sudeste,3550308," "1004,11,7107", "99999999,,,,,,,,,,,,", "20040020,Avenida Rio Branco,,,Centro,Rio de Janeiro,RJ," "Rio de Janeiro,Sudeste,3304557,,21,6001"}; static const char *ceps[] = {"01001000", "99999999", "20040020"}; fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; char *body; ts_reply_t reply; fixture_up(&fixture); body = csv_body(rows, 3); reply.status = 200; reply.body = body; reply.headers = NULL; ts_respond_default(fixture.server, &reply, 1); CHECK(cepis_lookup_many(fixture.client, ceps, 3, &list, &err) == CEPIS_OK); CHECK(list.count == 3); if (list.count == 3) { CHECK(list.items[0] != NULL); if (list.items[0] != NULL) { CHECK_STR(list.items[0]->localidade, "S\xC3\xA3o Paulo"); CHECK(list.items[0]->geo == NULL); CHECK(list.items[0]->raw == NULL); } CHECK(list.items[1] == NULL); CHECK(list.items[2] != NULL); if (list.items[2] != NULL) CHECK_STR(list.items[2]->uf, "RJ"); } CHECK(strstr(ts_path(fixture.server, 0), "/csv?ceps=") == ts_path(fixture.server, 0)); cepis_address_list_free(&list); free(body); fixture_down(&fixture); } static void test_batch_field_with_comma_is_parsed(void) { static const char *rows[] = { "01001000,\"Rua Um, Dois\",,,Centro,S\xC3\xA3o Paulo,SP," "S\xC3\xA3o Paulo,Sudeste,1,,11,1"}; static const char *ceps[] = {"01001000"}; fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; char *body; ts_reply_t reply; fixture_up(&fixture); body = csv_body(rows, 1); reply.status = 200; reply.body = body; reply.headers = NULL; ts_respond_default(fixture.server, &reply, 1); CHECK(cepis_lookup_many(fixture.client, ceps, 1, &list, &err) == CEPIS_OK); CHECK(list.count == 1 && list.items[0] != NULL); if (list.count == 1 && list.items[0] != NULL) CHECK_STR(list.items[0]->logradouro, "Rua Um, Dois"); cepis_address_list_free(&list); free(body); fixture_down(&fixture); } static void test_batch_empty_list_makes_no_request(void) { fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; fixture_up(&fixture); CHECK(cepis_lookup_many(fixture.client, NULL, 0, &list, &err) == CEPIS_OK); CHECK(list.count == 0 && list.items == NULL); CHECK(ts_total(fixture.server) == 0); cepis_address_list_free(&list); fixture_down(&fixture); } static void test_batch_chunks_above_the_api_limit(void) { fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; char **ceps = malloc(501 * sizeof(*ceps)); char **rows = malloc(501 * sizeof(*rows)); ts_reply_t replies[2]; char *first; char *second; size_t i; fixture_up(&fixture); for (i = 0; i < 501; i++) { ceps[i] = malloc(16); rows[i] = malloc(64); snprintf(ceps[i], 16, "%08zu", i); snprintf(rows[i], 64, "%s,,,,,X,SP,S\xC3\xA3o Paulo,Sudeste,1,,11,1", ceps[i]); } first = csv_body((const char *const *)rows, 500); second = csv_body((const char *const *)rows + 500, 1); replies[0].status = 200; replies[0].body = first; replies[0].headers = NULL; replies[1].status = 200; replies[1].body = second; replies[1].headers = NULL; ts_respond_default(fixture.server, replies, 2); CHECK(cepis_lookup_many(fixture.client, (const char *const *)ceps, 501, &list, &err) == CEPIS_OK); CHECK(ts_total(fixture.server) == 2); CHECK(list.count == 501); if (list.count == 501) { CHECK(list.items[0] != NULL); CHECK(list.items[499] != NULL); CHECK(list.items[500] != NULL); } cepis_address_list_free(&list); for (i = 0; i < 501; i++) { free(ceps[i]); free(rows[i]); } free(ceps); free(rows); free(first); free(second); fixture_down(&fixture); } static void test_batch_truncated_response_does_not_shift(void) { static const char *rows[] = { "01001000,,,,,S\xC3\xA3o Paulo,SP,S\xC3\xA3o " "Paulo,Sudeste,1,,11,1"}; static const char *ceps[] = {"01001000", "20040020"}; fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; char *body; ts_reply_t reply; fixture_up(&fixture); body = csv_body(rows, 1); reply.status = 200; reply.body = body; reply.headers = NULL; ts_respond_default(fixture.server, &reply, 1); CHECK(cepis_lookup_many(fixture.client, ceps, 2, &list, &err) == CEPIS_OK); CHECK(list.count == 2); if (list.count == 2) { CHECK(list.items[0] != NULL); CHECK(list.items[1] == NULL); } cepis_address_list_free(&list); free(body); fixture_down(&fixture); } static void test_batch_malformed_cep_becomes_null(void) { static const char *rows[] = { "01001000,,,,,S\xC3\xA3o Paulo,SP,S\xC3\xA3o " "Paulo,Sudeste,1,,11,1", "123,,,,,,,,,,,,"}; static const char *ceps[] = {"01001000", "123"}; fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; char *body; ts_reply_t reply; fixture_up(&fixture); body = csv_body(rows, 2); reply.status = 200; reply.body = body; reply.headers = NULL; ts_respond_default(fixture.server, &reply, 1); CHECK(cepis_lookup_many(fixture.client, ceps, 2, &list, &err) == CEPIS_OK); CHECK(list.count == 2); if (list.count == 2) CHECK(list.items[1] == NULL); cepis_address_list_free(&list); free(body); fixture_down(&fixture); } static void test_batch_retries_on_503(void) { static const char *rows[] = { "01001000,,,,,S\xC3\xA3o Paulo,SP,S\xC3\xA3o " "Paulo,Sudeste,1,,11,1"}; static const char *ceps[] = {"01001000"}; fixture_t fixture; cepis_address_list_t list = {NULL, 0}; cepis_error_t err; ts_reply_t replies[2]; char *body; fixture_up(&fixture); body = csv_body(rows, 1); replies[0].status = 503; replies[0].body = "{\"erro\":\"interno\"}"; replies[0].headers = NULL; replies[1].status = 200; replies[1].body = body; replies[1].headers = NULL; ts_respond_default(fixture.server, replies, 2); CHECK(cepis_lookup_many(fixture.client, ceps, 1, &list, &err) == CEPIS_OK); CHECK(ts_total(fixture.server) == 2); CHECK(list.count == 1 && list.items[0] != NULL); cepis_address_list_free(&list); free(body); fixture_down(&fixture); } static void test_health(void) { fixture_t fixture; cepis_health_t *health = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/health", 200, "{\"status\":\"ok\",\"dne\":\"26071\",\"ceps\":1596168," "\"geo\":894489,\"base_gerada_em\":" "\"2026-07-24T03:48:28Z\"}", NULL); CHECK(cepis_health(fixture.client, &health, &err) == CEPIS_OK); if (health != NULL) { CHECK(cepis_health_ok(health)); CHECK_STR(health->dne, "26071"); CHECK(health->ceps == 1596168); CHECK(health->geo == 894489); CHECK_STR(health->base_gerada_em, "2026-07-24T03:48:28Z"); } cepis_health_free(health); fixture_down(&fixture); } static void test_health_degraded_does_not_fail(void) { fixture_t fixture; cepis_health_t *health = NULL; cepis_error_t err; fixture_up(&fixture); ts_respond(fixture.server, "/health", 503, "{\"status\":\"degradado\",\"dne\":\"\",\"ceps\":0," "\"geo\":0}", NULL); CHECK(cepis_health(fixture.client, &health, &err) == CEPIS_OK); CHECK(health != NULL && !cepis_health_ok(health)); CHECK(ts_count(fixture.server, "/health") == 1); cepis_health_free(health); fixture_down(&fixture); } static void test_trailing_slash_is_not_duplicated(void) { cepis_client_t *client = NULL; cepis_config_t config; cepis_error_t err; cepis_config_init(&config); config.base_url = "http://example.invalid///"; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_OK); CHECK_STR(cepis_client_base_url(client), "http://example.invalid"); cepis_client_free(client); } static void test_invalid_configuration_is_rejected(void) { cepis_client_t *client = NULL; cepis_config_t config; cepis_error_t err; cepis_config_init(&config); config.max_attempts = 0; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_ERR_CONFIG); cepis_config_init(&config); config.timeout = 0.0; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_ERR_CONFIG); cepis_config_init(&config); config.base_url = ""; CHECK(cepis_client_new(&config, &client, &err) == CEPIS_ERR_CONFIG); CHECK(client == NULL); } static void test_user_agent_identifies_the_library(void) { cepis_client_t *client = NULL; cepis_error_t err; CHECK(cepis_client_new(NULL, &client, &err) == CEPIS_OK); CHECK(strstr(cepis_client_user_agent(client), "cepis-c/") == cepis_client_user_agent(client)); CHECK(strstr(cepis_client_user_agent(client), CEPIS_VERSION) != NULL); CHECK_STR(cepis_client_base_url(client), CEPIS_DEFAULT_BASE_URL); cepis_client_free(client); } static void test_status_strings_exist(void) { CHECK_STR(cepis_status_str(CEPIS_OK), "ok"); CHECK_STR(cepis_status_str(CEPIS_ERR_NOT_FOUND), "CEP not found"); CHECK_STR(cepis_status_str(CEPIS_ERR_RATE_LIMITED), "rate limited"); CHECK(cepis_status_str(CEPIS_ERR_NOMEM) != NULL); } int main(void) { cepis_global_init(); RUN(test_normalize_accepts_masked_and_unmasked); RUN(test_normalize_rejects_bad_format); RUN(test_format_cep); RUN(test_successful_lookup); RUN(test_escaped_and_utf8_strings_decode); RUN(test_reads_the_coordinate_when_present); RUN(test_municipality_coordinate_is_not_precise); RUN(test_malformed_geo_does_not_lose_the_address); RUN(test_new_api_field_does_not_break); RUN(test_not_found_is_not_retried); RUN(test_not_found_names_the_municipality); RUN(test_not_found_without_faixa_leaves_it_absent); RUN(test_malformed_faixa_is_still_a_clean_not_found); RUN(test_faixa_does_not_survive_the_next_error); RUN(test_invalid_cep_never_leaves_the_machine); RUN(test_api_400_becomes_invalid_cep); RUN(test_response_that_is_not_json); RUN(test_retries_on_500_then_succeeds); RUN(test_exhausts_attempts_then_fails); RUN(test_max_attempts_one_disables_retry); RUN(test_429_becomes_rate_limited_with_retry_after); RUN(test_retry_after_http_date_is_understood); RUN(test_retry_after_honours_the_cap); RUN(test_network_error_when_nothing_listens); RUN(test_total_timeout_bounds_the_whole_call); RUN(test_batch_preserves_order_and_marks_missing); RUN(test_batch_field_with_comma_is_parsed); RUN(test_batch_empty_list_makes_no_request); RUN(test_batch_chunks_above_the_api_limit); RUN(test_batch_truncated_response_does_not_shift); RUN(test_batch_malformed_cep_becomes_null); RUN(test_batch_retries_on_503); RUN(test_health); RUN(test_health_degraded_does_not_fail); RUN(test_trailing_slash_is_not_duplicated); RUN(test_invalid_configuration_is_rejected); RUN(test_user_agent_identifies_the_library); RUN(test_status_strings_exist); cepis_global_cleanup(); printf("\n%d test(s), %d failed\n", tests_run, tests_failed); return tests_failed == 0 ? 0 : 1; }