diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:32:01 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:32:01 -0300 |
| commit | b199ae044f8f9cd4b745214cd5e5495f86b5eb54 (patch) | |
| tree | a8e7a134e77381b11091126a8946208e2e6afbcb /tests/test_cepis.py | |
| parent | a82459630e957a8d94c375394dae66734f3c90c1 (diff) | |
| download | python-5295ba01f58987277c1e8531fc62619658f17eb8.tar.gz | |
A failing CEP is rarely random: it is someone who knows the city and
guessed the number, 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, so
a caller can ask "did you mean somewhere in Areal/RJ?" instead of
"invalid CEP".
CEPNotFound gains faixa, defaulting to None. It stays a 404 and stays
an exception — the field is context for the error, never a demotion of
it into a partial success. Parsing is deliberately total: anything
missing, empty or malformed under faixa yields None, because turning a
clean not-found into a parse failure over an optional courtesy field
would be strictly worse than not having the field at all.
Faixa's cep_ini/cep_fim are the smallest and largest CEP that actually
exist there, not the bounds the Correios publish, and the range in
between is sparse — hence ceps, so the pair is never read as a list of
valid numbers.
Diffstat (limited to 'tests/test_cepis.py')
| -rw-r--r-- | tests/test_cepis.py | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/tests/test_cepis.py b/tests/test_cepis.py index dc27179..31df5e9 100644 --- a/tests/test_cepis.py +++ b/tests/test_cepis.py @@ -46,6 +46,20 @@ SE_ADDRESS = { "siafi": "7107", } +# The `faixa` block of a 404: 25845000 is the CEP the Correios publish as the +# start of Areal/RJ and the one the DNE does not actually have, which is why +# cep_ini is 25845001. +AREAL_FAIXA = { + "localidade": "Areal", + "uf": "RJ", + "estado": "Rio de Janeiro", + "regiao": "Sudeste", + "ibge": "3300225", + "cep_ini": "25845001", + "cep_fim": "25849899", + "ceps": 118, +} + CSV_HEADER = ( "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi" ) @@ -252,6 +266,101 @@ class TestLookup(BaseTest): self.client.lookup("01001000") +class TestFaixa(BaseTest): + """The municipality carried by a 404 — context for the error, not a hit.""" + + def _not_found(self, path: str, **extra: object) -> None: + body: dict[str, object] = { + "erro": "cep_nao_encontrado", + "mensagem": "CEP não existe na base do DNE", + } + body.update(extra) + self.routes.respond(path, (404, json.dumps(body), {})) + + def test_reads_the_faixa_when_present(self) -> None: + self._not_found("/25845000", faixa=AREAL_FAIXA) + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("25845000") + + faixa = ctx.exception.faixa + assert faixa is not None + self.assertEqual(faixa.localidade, "Areal") + self.assertEqual(faixa.uf, "RJ") + self.assertEqual(faixa.ibge, "3300225") + self.assertEqual(faixa.label, "Areal/RJ") + + def test_bounds_are_the_ceps_that_exist_and_the_range_is_sparse(self) -> None: + """118 CEPs across ~4,900 numbers: the pair is not a list of valid CEPs.""" + self._not_found("/25845000", faixa=AREAL_FAIXA) + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("25845000") + + faixa = ctx.exception.faixa + assert faixa is not None + self.assertEqual(faixa.cep_ini, "25845001") + self.assertEqual(faixa.cep_fim, "25849899") + self.assertEqual(faixa.ceps, 118) + span = int(faixa.cep_fim) - int(faixa.cep_ini) + 1 + self.assertLess(faixa.ceps, span) + # The CEP that failed is the DNE's own published start of the range, + # and it is below cep_ini precisely because it does not exist. + self.assertLess(int("25845000"), int(faixa.cep_ini)) + + def test_message_names_the_municipality(self) -> None: + self._not_found("/25845000", faixa=AREAL_FAIXA) + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("25845000") + self.assertIn("Areal/RJ", str(ctx.exception)) + self.assertNotIn('"', str(ctx.exception)) + + def test_absent_faixa_is_none(self) -> None: + """Omitted, never null: a band no municipality owns just has no faixa.""" + self._not_found("/99999999") + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("99999999") + self.assertIsNone(ctx.exception.faixa) + self.assertIn("99999999", str(ctx.exception)) + + def test_still_a_not_found_with_a_faixa(self) -> None: + """A faixa must never promote a 404 into an address.""" + self._not_found("/25845000", faixa=AREAL_FAIXA) + with self.assertRaises(KeyError): + self.client.lookup("25845000") + + def test_faixa_is_not_retried(self) -> None: + self._not_found("/25845000", faixa=AREAL_FAIXA) + with self.assertRaises(CEPNotFound): + self.client.lookup("25845000") + self.assertEqual(self.routes.count("/25845000"), 1) + + def test_malformed_faixa_still_raises_a_clean_not_found(self) -> None: + """Whatever arrives under `faixa`, the exception is the same exception.""" + for junk in ({}, [], "Areal", 42, {"uf": "RJ"}, {"localidade": ""}): + with self.subTest(faixa=junk): + self.routes.respond( + "/25845000", + (404, json.dumps({"erro": "cep_nao_encontrado", "faixa": junk}), {}), + ) + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("25845000") + self.assertIsNone(ctx.exception.faixa) + + def test_bad_ceps_count_yields_none(self) -> None: + """A field that cannot be read makes the whole block untrustworthy.""" + faixa = dict(AREAL_FAIXA, ceps="não é número") + self._not_found("/25845000", faixa=faixa) + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("25845000") + self.assertIsNone(ctx.exception.faixa) + + def test_unparseable_404_body_does_not_become_a_parse_error(self) -> None: + """An HTML 404 from a proxy is still a CEPNotFound, not InvalidResponse.""" + self.routes.respond("/99999999", (404, "<html>not found</html>", {})) + with self.assertRaises(CEPNotFound) as ctx: + self.client.lookup("99999999") + self.assertIsNone(ctx.exception.faixa) + + class TestRetry(BaseTest): def test_retries_on_500_then_succeeds(self) -> None: self.routes.respond( |