diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:33:24 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:33:24 -0300 |
| commit | d7ca2d6f4b1c5c1f5f1b6ce6ffad35aaffee5b0f (patch) | |
| tree | 21aca875c28f4d7d9ec3b0a807e7c7f15a6766d8 /Tests/CepisTests | |
| parent | 72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7 (diff) | |
| download | swift-main.tar.gz | |
A CEP that fails a lookup is rarely a random number: it is almost always
someone who knows the city and guessed the digits. The classic guess is
the -000, which 957 municipalities never received. Answering that with a
bare "not found" throws away what the API knows — that the number is in
Areal/RJ's band — and leaves the caller unable to tell a typo apart from
a city that has no such CEP.
The 404 now carries an optional faixa, so cepNotFound carries it too. It
decorates the error and never replaces it: the CEP still does not exist
and the call still throws. Parsing is deliberately incapable of
escalating — a faixa that is absent, null, empty or malformed becomes
nil, because losing the decoration must never cost the clean not-found
underneath it.
Faixa offers no contains(): cep_ini and cep_fim are the smallest and
largest CEP that actually exist, not the bounds the Correios publish,
and the range between them is sparse — Areal's 118 CEPs are spread over
some 4,900 numbers. Only a lookup can answer whether a CEP exists, and
an API that appeared to answer it from the pair would be lying.
Widening cepNotFound's payload breaks anyone matching the case with a
single binding, hence 0.2.0.
Diffstat (limited to 'Tests/CepisTests')
| -rw-r--r-- | Tests/CepisTests/CepisTests.swift | 165 |
1 files changed, 163 insertions, 2 deletions
diff --git a/Tests/CepisTests/CepisTests.swift b/Tests/CepisTests/CepisTests.swift index 46699bc..deabead 100644 --- a/Tests/CepisTests/CepisTests.swift +++ b/Tests/CepisTests/CepisTests.swift @@ -14,6 +14,14 @@ let seAddressJSON = """ "ddd":"11","siafi":"7107"} """ +/// The 404 for `25845000`: the CEP the DNE itself lists as Areal's first, and +/// which nonetheless does not exist. +let arealNotFoundJSON = """ + {"erro":"cep_nao_encontrado","mensagem":"CEP não 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}} + """ + let csvHeader = "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi" @@ -65,7 +73,7 @@ final class NormalisationTests: XCTestCase { func testInvalidCEPIsNotRetriable() { XCTAssertFalse(CepisError.invalidCEP("x").isRetriable) - XCTAssertFalse(CepisError.cepNotFound("x").isRetriable) + XCTAssertFalse(CepisError.cepNotFound(message: "x", faixa: nil).isRetriable) XCTAssertTrue(CepisError.networkError(message: "x").isRetriable) } } @@ -142,8 +150,9 @@ final class LookupTests: ServerBackedTestCase { do { _ = try await client.lookup("99999999") XCTFail("expected cepNotFound") - } catch CepisError.cepNotFound(let message) { + } catch CepisError.cepNotFound(let message, let faixa) { XCTAssertTrue(message.contains("99999999")) + XCTAssertNil(faixa) } } @@ -188,6 +197,158 @@ final class LookupTests: ServerBackedTestCase { } } +final class NotFoundFaixaTests: ServerBackedTestCase { + /// Serves `body` as the 404 for `cep` and returns the faixa that survived. + private func faixaFromNotFound(_ body: String, cep: String = "25845000") async throws -> Faixa? + { + routes.respond("/\(cep)", status: 404, body: body) + do { + _ = try await client.lookup(cep) + XCTFail("expected cepNotFound") + return nil + } catch CepisError.cepNotFound(_, let faixa) { + return faixa + } + } + + func testNamesTheMunicipalityTheCEPWouldBelongTo() async throws { + let decoded = try await faixaFromNotFound(arealNotFoundJSON) + let faixa = try XCTUnwrap(decoded) + XCTAssertEqual(faixa.localidade, "Areal") + XCTAssertEqual(faixa.uf, "RJ") + XCTAssertEqual(faixa.estado, "Rio de Janeiro") + XCTAssertEqual(faixa.regiao, "Sudeste") + XCTAssertEqual(faixa.ibge, "3300225") + XCTAssertEqual(faixa.localidadeUF, "Areal/RJ") + } + + /// The faixa decorates the 404; it must never promote it to a success. + func testStillThrowsAndReturnsNoAddress() async throws { + routes.respond("/25845000", status: 404, body: arealNotFoundJSON) + do { + let address = try await client.lookup("25845000") + XCTFail("a faixa must not turn a not-found into \(address)") + } catch CepisError.cepNotFound(let message, let faixa) { + XCTAssertTrue(message.contains("25845000")) + XCTAssertTrue(message.contains("does not exist")) + XCTAssertNotNil(faixa) + } + } + + /// `cep_ini` is the first CEP that *exists*, not the DNE's published start + /// — which for Areal is `25845000`, the very CEP that just failed. + func testBoundsAreTheCEPsThatExistNotTheDNEStart() async throws { + let decoded = try await faixaFromNotFound(arealNotFoundJSON) + let faixa = try XCTUnwrap(decoded) + XCTAssertEqual(faixa.cepIni, "25845001") + XCTAssertEqual(faixa.cepFim, "25849899") + XCTAssertNotEqual(faixa.cepIni, "25845000") + } + + /// The pair is not a list: 118 CEPs across ~4,900 numbers. + func testRangeIsSparse() async throws { + let decoded = try await faixaFromNotFound(arealNotFoundJSON) + let faixa = try XCTUnwrap(decoded) + XCTAssertEqual(faixa.ceps, 118) + + let first = try XCTUnwrap(Int(faixa.cepIni)) + let last = try XCTUnwrap(Int(faixa.cepFim)) + let span = last - first + 1 + XCTAssertEqual(span, 4899) + XCTAssertLessThan(faixa.ceps, span / 10) + } + + func testAbsentWhenNoMunicipalityOwnsTheBand() async throws { + let body = #"{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}"# + let decoded = try await faixaFromNotFound(body) + XCTAssertNil(decoded) + } + + func testNullFaixaIsNil() async throws { + let decoded = try await faixaFromNotFound(#"{"erro":"cep_nao_encontrado","faixa":null}"#) + XCTAssertNil(decoded) + } + + func testEmptyFaixaIsNil() async throws { + let decoded = try await faixaFromNotFound(#"{"erro":"cep_nao_encontrado","faixa":{}}"#) + XCTAssertNil(decoded) + } + + func testFaixaWithoutAMunicipalityIsNil() async throws { + for value in [#""""#, #"" ""#, "123", "null", "[]"] { + let body = #"{"erro":"cep_nao_encontrado","faixa":{"localidade":\#(value),"uf":"RJ"}}"# + let decoded = try await faixaFromNotFound(body) + XCTAssertNil(decoded, "localidade \(value)") + } + } + + func testMalformedFaixaDoesNotBecomeADecodingFailure() async throws { + let bodies = [ + #"{"erro":"cep_nao_encontrado","faixa":"Areal"}"#, + #"{"erro":"cep_nao_encontrado","faixa":[{"localidade":"Areal"}]}"#, + #"{"erro":"cep_nao_encontrado","faixa":{"localidade":{"nome":"Areal"}}}"#, + ] + for body in bodies { + let decoded = try await faixaFromNotFound(body) + XCTAssertNil(decoded, body) + } + } + + /// A 404 body that is not JSON at all is still a clean not-found, never an + /// `invalidResponse`. + func testNonJSONBodyStillYieldsNotFound() async throws { + for body in ["<html>captive portal</html>", ""] { + let decoded = try await faixaFromNotFound(body) + XCTAssertNil(decoded, body) + } + } + + func testToleratesOddFieldTypesAroundTheMunicipality() async throws { + let body = """ + {"erro":"cep_nao_encontrado","faixa":{"localidade":"Areal","uf":"RJ", + "ibge":3300225,"cep_ini":"25845001","ceps":118.0}} + """ + let decoded = try await faixaFromNotFound(body) + let faixa = try XCTUnwrap(decoded) + XCTAssertEqual(faixa.localidade, "Areal") + XCTAssertEqual(faixa.ceps, 118) + XCTAssertEqual(faixa.ibge, "") + XCTAssertEqual(faixa.cepFim, "") + } + + func testDescriptionMentionsTheMunicipality() async throws { + routes.respond("/25845000", status: 404, body: arealNotFoundJSON) + do { + _ = try await client.lookup("25845000") + XCTFail("expected cepNotFound") + } catch let error as CepisError { + XCTAssertTrue(error.description.contains("Areal/RJ"), error.description) + XCTAssertEqual(error.errorDescription, error.description) + XCTAssertEqual(error.faixa?.localidade, "Areal") + // `message` stays what it always was, for callers matching on it. + XCTAssertFalse(error.message.contains("Areal")) + } + } + + func testDescriptionWithoutAFaixaIsJustTheMessage() { + let error = CepisError.cepNotFound(message: "CEP 99999999 does not exist", faixa: nil) + XCTAssertEqual(error.description, error.message) + XCTAssertEqual(error.errorDescription, error.message) + XCTAssertNil(error.faixa) + } + + func testFaixaAccessorIsNilForTheOtherCases() { + XCTAssertNil(CepisError.invalidCEP("x").faixa) + XCTAssertNil(CepisError.networkError(message: "x").faixa) + XCTAssertNil(CepisError.serverError(message: "x", status: 500).faixa) + } + + func testAFaixaIsStillNotRetriable() { + let faixa = Faixa(localidade: "Areal", uf: "RJ") + XCTAssertFalse(CepisError.cepNotFound(message: "x", faixa: faixa).isRetriable) + } +} + final class RetryTests: ServerBackedTestCase { func testRetriesOn500ThenSucceeds() async throws { routes.respond( |