summaryrefslogtreecommitdiffstats
path: root/tests/test_cepis.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_cepis.c')
-rw-r--r--tests/test_cepis.c935
1 files changed, 935 insertions, 0 deletions
diff --git a/tests/test_cepis.c b/tests/test_cepis.c
new file mode 100644
index 0000000..ee23305
--- /dev/null
+++ b/tests/test_cepis.c
@@ -0,0 +1,935 @@
+#include "cepis.h"
+#include "test_server.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+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_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,
+ "<html>captive portal</html>", 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_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;
+}