From 82d92ed4291663bae94014b4363064dbb26c6a51 Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:25:04 -0300 Subject: Name the municipality behind a CEP that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CEP that fails a lookup is rarely a random number: it is usually someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now attaches a faixa block to the 404 naming the municipality that band belongs to, so the caller can answer "no, but you were looking at Areal/RJ" instead of just "no". It stays an error. Faixa hangs off CepNotFoundException, never off Address, and cepIni/cepFim are the extremes that actually exist in the municipality rather than the range the Correios publish — echoing back the published start would return the very CEP that just failed. The interval is sparse, which is what ceps is for; it is not a list of alternatives. Parsing is deliberately forgiving: a missing, empty or malformed block is null. Losing a clean not-found to a parse error over the hint attached to it would be a bad trade. --- test/cepis_test.dart | 166 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 155 insertions(+), 11 deletions(-) (limited to 'test') diff --git a/test/cepis_test.dart b/test/cepis_test.dart index 54df88f..e09601f 100644 --- a/test/cepis_test.dart +++ b/test/cepis_test.dart @@ -23,6 +23,23 @@ const Map seAddress = { 'siafi': '7107', }; +const Map arealFaixa = { + 'localidade': 'Areal', + 'uf': 'RJ', + 'estado': 'Rio de Janeiro', + 'regiao': 'Sudeste', + 'ibge': '3300225', + 'cep_ini': '25845001', + 'cep_fim': '25849899', + 'ceps': 118, +}; + +String notFound({Map? faixa}) => jsonEncode({ + 'erro': 'cep_nao_encontrado', + 'mensagem': 'CEP não existe na base do DNE', + if (faixa != null) 'faixa': faixa, + }); + const String csvHeader = 'cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,' 'ibge,gia,ddd,siafi'; @@ -184,16 +201,7 @@ void main() { }); test('404 becomes CepNotFoundException and is not retried', () async { - server.routes.respond( - '/99999999', - Reply( - 404, - jsonEncode({ - 'erro': 'cep_nao_encontrado', - 'mensagem': 'CEP não existe na base do DNE', - }), - ), - ); + server.routes.respond('/99999999', Reply(404, notFound())); await expectLater( client.lookup('99999999'), @@ -208,6 +216,127 @@ void main() { expect(server.routes.count('/99999999'), 1); }); + test('a 404 with no faixa leaves it null', () async { + server.routes.respond('/99999999', Reply(404, notFound())); + + await expectLater( + client.lookup('99999999'), + throwsA( + isA().having( + (CepNotFoundException e) => e.faixa, + 'faixa', + isNull, + ), + ), + ); + }); + + test('a 404 carries the municipality the CEP would belong to', () async { + server.routes.respond( + '/25845000', + Reply(404, notFound(faixa: arealFaixa)), + ); + + await expectLater( + client.lookup('25845000'), + throwsA( + isA() + .having((CepNotFoundException e) => e.cep, 'cep', '25845000') + .having( + (CepNotFoundException e) => e.faixa, + 'faixa', + const Faixa( + localidade: 'Areal', + uf: 'RJ', + estado: 'Rio de Janeiro', + regiao: 'Sudeste', + ibge: '3300225', + cepIni: '25845001', + cepFim: '25849899', + ceps: 118, + ), + ), + ), + ); + }); + + test('the faixa does not turn a 404 into an address', () async { + server.routes.respond( + '/25845000', + Reply(404, notFound(faixa: arealFaixa)), + ); + + await expectLater( + client.lookup('25845000'), + throwsA(isA()), + ); + expect(server.routes.count('/25845000'), 1); + }); + + test('the message names the municipality when there is one', () async { + server.routes.respond( + '/25845000', + Reply(404, notFound(faixa: arealFaixa)), + ); + + try { + await client.lookup('25845000'); + fail('expected CepNotFoundException'); + } on CepNotFoundException catch (error) { + expect(error.toString(), contains('Areal/RJ')); + expect(error.toString(), contains('118 CEPs')); + } + }); + + test('an unreadable faixa is null, not a failure', () async { + final bodies = [ + notFound(faixa: {}), + notFound(faixa: {'uf': 'RJ'}), + jsonEncode({ + 'erro': 'cep_nao_encontrado', + 'faixa': 'Areal', + }), + jsonEncode({ + 'erro': 'cep_nao_encontrado', + 'faixa': [], + }), + 'portal', + '', + ]; + + for (final String body in bodies) { + server.routes.respond('/99999999', Reply(404, body)); + + await expectLater( + client.lookup('99999999'), + throwsA( + isA().having( + (CepNotFoundException e) => e.faixa, + 'faixa', + isNull, + ), + ), + reason: body, + ); + } + }); + + test('a faixa missing the counts still names the municipality', () async { + server.routes.respond( + '/25845000', + Reply(404, notFound(faixa: {'localidade': 'Areal'})), + ); + + try { + await client.lookup('25845000'); + fail('expected CepNotFoundException'); + } on CepNotFoundException catch (error) { + expect(error.faixa!.localidade, 'Areal'); + expect(error.faixa!.uf, ''); + expect(error.faixa!.ceps, 0); + } + }); + test('an invalid CEP never leaves the machine', () async { await expectLater( client.lookup('123'), @@ -265,7 +394,7 @@ void main() { server.routes.respond('/01001000', Reply(200, jsonEncode(seAddress))); await client.lookup('01001000'); - expect(server.routes.userAgents.single, startsWith('cepis-dart/0.1.0')); + expect(server.routes.userAgents.single, startsWith('cepis-dart/0.2.0')); }); }); @@ -628,5 +757,20 @@ void main() { expect(one, two); expect(one.hashCode, two.hashCode); }); + + test('two identical faixas are equal', () { + final one = Faixa.fromJson(Map.of(arealFaixa)); + final two = Faixa.fromJson(Map.of(arealFaixa)); + expect(one, two); + expect(one.hashCode, two.hashCode); + }); + + test('a faixa without a municipality is nothing to report', () { + expect(Faixa.fromJson({}), isNull); + expect( + Faixa.fromJson({'localidade': '', 'uf': 'RJ'}), + isNull, + ); + }); }); } -- cgit v1.3