summaryrefslogtreecommitdiffstats
path: root/t/09-faixa.t
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:34:36 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:34:36 -0300
commitf2f3e5b5b22f5db49670391fb747a52fd492b9db (patch)
tree3d02d13f3111bdd7381470d3fd3db1f58e00bc8c /t/09-faixa.t
parent0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (diff)
downloadperl-f2f3e5b5b22f5db49670391fb747a52fd492b9db.tar.gz
Carry the municipality on a CEP that does not existHEADv0.2.0main
A CEP that fails 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 answers those 404s with a faixa block naming the municipality that owns the band, so the library can say "25845000 does not exist in Areal/RJ" instead of "not found". It stays an error: lookup still throws Cepis::Error::CEPNotFound, and the faixa hangs off the exception rather than turning a miss into an address. Parsing it is deliberately forgiving — a missing, empty or malformed block yields undef, because losing the clean not-found to a parse failure would be a worse outcome than losing the context. cep_ini and cep_fim are the smallest and largest CEP that actually exist, not the bounds the Correios publish, and the interval between them is sparse. That is why ceps is exposed alongside them and why the docs say twice that the pair is not a list of valid CEPs.
Diffstat (limited to 't/09-faixa.t')
-rw-r--r--t/09-faixa.t178
1 files changed, 178 insertions, 0 deletions
diff --git a/t/09-faixa.t b/t/09-faixa.t
new file mode 100644
index 0000000..ea3f76a
--- /dev/null
+++ b/t/09-faixa.t
@@ -0,0 +1,178 @@
+use strict;
+use warnings;
+use utf8;
+
+use FindBin ();
+use lib "$FindBin::Bin/lib";
+
+use Test::More;
+use TestServer qw(json new_client);
+
+use Cepis;
+
+sub areal {
+ return {
+ localidade => 'Areal',
+ uf => 'RJ',
+ estado => 'Rio de Janeiro',
+ regiao => 'Sudeste',
+ ibge => '3300225',
+ cep_ini => '25845001',
+ cep_fim => '25849899',
+ ceps => 118,
+ };
+}
+
+sub not_found {
+ my (%extra) = @_;
+ return json(
+ { erro => 'cep_nao_encontrado',
+ mensagem => 'CEP não existe na base do DNE',
+ %extra,
+ }
+ );
+}
+
+# The 404 that knows where the CEP would have been.
+{
+ my $server = TestServer->new(
+ routes => { '/25845000' => [ 404, not_found( faixa => areal() ), {} ] } );
+ my $client = new_client($server);
+
+ my $ok = eval { $client->lookup('25845000'); 1 };
+ my $err = $@;
+
+ ok( !$ok, 'a CEP with a faixa still throws' );
+ isa_ok( $err, 'Cepis::Error::CEPNotFound' );
+
+ my $faixa = $err->faixa;
+ isa_ok( $faixa, 'Cepis::Faixa' );
+ is( $faixa->localidade, 'Areal', 'localidade' );
+ is( $faixa->uf, 'RJ', 'uf' );
+ is( $faixa->estado, 'Rio de Janeiro', 'estado' );
+ is( $faixa->regiao, 'Sudeste', 'regiao' );
+ is( $faixa->ibge, '3300225', 'ibge' );
+ is( $faixa->cep_ini, '25845001', 'cep_ini' );
+ is( $faixa->cep_fim, '25849899', 'cep_fim' );
+ is( $faixa->ceps, 118, 'ceps counts what actually exists' );
+ is( $faixa->label, 'Areal/RJ', 'label is ready to print' );
+
+ like( "$err", qr/Areal/, 'the message names the municipality' );
+ like( "$err", qr/25845000/, 'and still names the CEP' );
+ is( $server->count('/25845000'), 1, 'a 404 with a faixa is not retried' );
+
+ is_deeply( $faixa->to_hash, areal(), 'to_hash gives the eight fields back' );
+
+ # The DNE's own start for Areal is 25845000 — the CEP that just failed. What
+ # comes back is the smallest CEP that exists, which is a different number.
+ isnt( $faixa->cep_ini, '25845000',
+ 'cep_ini is not the published start of the band' );
+
+ # 118 CEPs across ~4,900 numbers: the range is sparse, and ceps is what says
+ # so. Nothing here may be read as a list of valid CEPs.
+ cmp_ok( $faixa->ceps, '<', $faixa->cep_fim - $faixa->cep_ini,
+ 'the range is far sparser than its span' );
+
+ $server->stop;
+}
+
+# The 404 that does not: a band no municipality owns. The API omits the field.
+{
+ my $server
+ = TestServer->new( routes => { '/99999999' => [ 404, not_found(), {} ] } );
+ my $client = new_client($server);
+
+ my $ok = eval { $client->lookup('99999999'); 1 };
+ my $err = $@;
+
+ ok( !$ok, 'a CEP with no faixa throws the same error' );
+ isa_ok( $err, 'Cepis::Error::CEPNotFound' );
+ is( $err->faixa, undef, 'and faixa is undef, because the API omitted it' );
+ like( "$err", qr/99999999/, 'the message names the CEP' );
+ unlike( "$err", qr/would fall in/, 'and promises nothing about where it was' );
+
+ $server->stop;
+}
+
+# A faixa that cannot be trusted must never cost the caller its clean 404.
+{
+ my @broken = (
+ [ 'null' => undef ],
+ [ 'an empty hash' => {} ],
+ [ 'a scalar' => 'Areal' ],
+ [ 'an array' => [ 'Areal', 'RJ' ] ],
+ [ 'no localidade' => { %{ areal() }, localidade => '' } ],
+ [ 'no ceps' => { map { $_ => areal()->{$_} } qw(localidade uf cep_ini cep_fim) } ],
+ [ 'a non-numeric ceps' => { %{ areal() }, ceps => 'many' } ],
+ [ 'a malformed cep_ini' => { %{ areal() }, cep_ini => '2584500' } ],
+ [ 'a masked cep_fim' => { %{ areal() }, cep_fim => '25849-899' } ],
+ [ 'a nested localidade' => { %{ areal() }, localidade => { name => 'Areal' } } ],
+ );
+
+ for my $case (@broken) {
+ my ( $label, $faixa ) = @$case;
+
+ my $server = TestServer->new(
+ routes => { '/25845000' => [ 404, not_found( faixa => $faixa ), {} ] } );
+ my $client = new_client($server);
+
+ my $ok = eval { $client->lookup('25845000'); 1 };
+ my $err = $@;
+
+ ok( !$ok, "a faixa that is $label still throws" );
+ isa_ok( $err, 'Cepis::Error::CEPNotFound' );
+ is( $err->faixa, undef, "a faixa that is $label is dropped" );
+
+ $server->stop;
+ }
+}
+
+# A 404 body that is not JSON at all — a proxy, an HTML page — is still a 404.
+{
+ my $server = TestServer->new(
+ routes => {
+ '/25845000' => [ 404, '<html>404 Not Found</html>', {} ],
+ '/25845002' => [ 404, '', {} ],
+ '/25845003' => [ 404, '[1,2,3]', {} ],
+ }
+ );
+ my $client = new_client($server);
+
+ for my $cep (qw(25845000 25845002 25845003)) {
+ my $ok = eval { $client->lookup($cep); 1 };
+ ok( !$ok, "an unparseable 404 body still throws for $cep" );
+ isa_ok( $@, 'Cepis::Error::CEPNotFound' );
+ is( $@->faixa, undef, 'with no faixa' );
+ }
+
+ $server->stop;
+}
+
+# from_json on its own, without a request in the way.
+{
+ isa_ok( Cepis::Faixa->from_json( areal() ), 'Cepis::Faixa' );
+ is( Cepis::Faixa->from_json(undef), undef, 'from_json(undef) is undef' );
+ is( Cepis::Faixa->from_json( [] ), undef, 'from_json on an array is undef' );
+ is( Cepis::Faixa->from_json('Areal'), undef, 'from_json on a string is undef' );
+
+ my $partial = Cepis::Faixa->from_json(
+ { localidade => 'Areal', cep_ini => '25845001', cep_fim => '25849899', ceps => 118 } );
+ isa_ok( $partial, 'Cepis::Faixa' );
+ is( $partial->uf, '', 'a missing uf is an empty string, as elsewhere' );
+ is( $partial->estado, '', 'and so is a missing estado' );
+ is( $partial->label, 'Areal', 'label falls back to the municipality alone' );
+
+ my $numeric = Cepis::Faixa->from_json( { %{ areal() }, ceps => '118' } );
+ is( $numeric->ceps, 118, 'ceps comes back as a number' );
+}
+
+# An unknown CEP with a faixa is still undef in a batch: the CSV path is
+# untouched by any of this.
+{
+ my $address = Cepis::Address->from_json( { %{ areal() }, cep => '25845000' } );
+ is( $address->localidade, 'Areal', 'Cepis::Address is unaffected' );
+ is( $address->geo, undef, 'and grows no faixa of its own' );
+ is( $address->field('cep_ini'), '25845001', 'stray fields land in raw as ever' );
+}
+
+done_testing();