diff options
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( |