summaryrefslogtreecommitdiffstats
path: root/tests/test_cepis.c
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:37:05 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:44:20 -0300
commiteb15ec9b2715783fcd2bf4f834595d1bd549c09d (patch)
treecd9c6e80892b409db346fde3fe8334f3c37a1130 /tests/test_cepis.c
parente2c6ae13af335b311da2429c903032124f926df2 (diff)
downloadc-a90051dad5efd24e867396ad840afa4d01a4d3ef.tar.gz
Report the municipality behind a not-found CEPHEADv0.2.0main
A CEP that fails is rarely a random number. It is usually someone who knows the city and guessed the last digits, and the classic guess is the -000 that 957 municipalities never received. The API now answers those 404s with a faixa block naming the municipality the CEP would have belonged to, and telling a caller "that CEP does not exist, did you mean somewhere in Areal/RJ?" is worth far more than telling them "no". It rides on cepis_error_t rather than on cepis_address_t because it is context for a failure, not an address: the lookup still returns CEPIS_ERR_NOT_FOUND and still hands back no address. Embedded by value with a has_faixa flag, so the error keeps owning nothing and keeps living on the stack with nothing to free; cepis_error_faixa gives the absent case the same NULL-or-not shape as cepis_address_t::geo, which is what the API's omit-never-null encoding calls for. Parsing is total on purpose. A body that is not JSON, a faixa that is null, a string, an array, or an object with no localidade all mean "the API named no municipality". A 404 is already the answer, and failing to read the garnish must never turn it into a different one. The bounds need reading with care, hence the length of the comments: cep_ini and cep_fim are the smallest and largest CEP that actually exist, not the range the Correios publish -- the DNE's own start for Areal is 25845000, exactly the CEP that just failed -- and the range is sparse, 118 CEPs scattered over the ~4900 numbers up to 25849899. That is what ceps is for.
Diffstat (limited to 'tests/test_cepis.c')
-rw-r--r--tests/test_cepis.c124
1 files changed, 124 insertions, 0 deletions
diff --git a/tests/test_cepis.c b/tests/test_cepis.c
index ee23305..b17ab67 100644
--- a/tests/test_cepis.c
+++ b/tests/test_cepis.c
@@ -310,6 +310,126 @@ static void test_not_found_is_not_retried(void)
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;
@@ -903,6 +1023,10 @@ int main(void)
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);