diff options
Diffstat (limited to 'tests/test_cepis.c')
| -rw-r--r-- | tests/test_cepis.c | 935 |
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; +} |