diff options
Diffstat (limited to 'internal/lookup/lookup_test.go')
| -rw-r--r-- | internal/lookup/lookup_test.go | 279 |
1 files changed, 0 insertions, 279 deletions
diff --git a/internal/lookup/lookup_test.go b/internal/lookup/lookup_test.go deleted file mode 100644 index 6eb7b4e..0000000 --- a/internal/lookup/lookup_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package lookup - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "sync/atomic" - "testing" - "time" - - cepis "cep.is/go" -) - -const areal404 = `{"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}}` - -const plain404 = `{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}` - -const address200 = `{"cep":"01001000","logradouro":"Praça da Sé","bairro":"Sé", - "localidade":"São Paulo","uf":"SP","estado":"São Paulo","regiao":"Sudeste","ddd":"11"}` - -// fakeAPI answers every request with the same status and body, and counts how -// many arrived: the count is what proves the extra request happens only on the -// 404 path. -type fakeAPI struct { - server *httptest.Server - requests atomic.Int64 - - // delay is applied from the delayFrom-th request on, which is how the - // second request — the one that reads the faixa — is made slow without - // slowing the lookup that precedes it. - delay time.Duration - delayFrom int64 -} - -func newFakeAPI(t *testing.T, status int, body string) *fakeAPI { - t.Helper() - f := &fakeAPI{delayFrom: 1} - f.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - n := f.requests.Add(1) - if f.delay > 0 && n >= f.delayFrom { - select { - case <-time.After(f.delay): - case <-r.Context().Done(): - return - } - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(status) - w.Write([]byte(body)) - })) - t.Cleanup(f.server.Close) - return f -} - -func newClient(t *testing.T, f *fakeAPI, opts ...Option) *Client { - t.Helper() - api, err := cepis.New( - cepis.WithBaseURL(f.server.URL), - cepis.WithMaxAttempts(1), - cepis.WithTimeout(2*time.Second), - ) - if err != nil { - t.Fatalf("cepis.New: %v", err) - } - return New(api, opts...) -} - -func TestNotFoundCarriesTheMunicipality(t *testing.T) { - f := newFakeAPI(t, http.StatusNotFound, areal404) - _, err := newClient(t, f).Lookup(context.Background(), "25845-000") - - if !errors.Is(err, cepis.ErrCEPNotFound) { - t.Fatalf("err = %v, want a not-found error", err) - } - faixa := FaixaOf(err) - if faixa == nil { - t.Fatal("the faixa on the 404 was dropped") - } - if faixa.Place() != "Areal/RJ" { - t.Fatalf("place = %q, want Areal/RJ", faixa.Place()) - } - if faixa.CEPs != 118 { - t.Fatalf("ceps = %d, want 118", faixa.CEPs) - } - if faixa.CEPIni != "25845001" || faixa.CEPFim != "25849899" { - t.Fatalf("range = %s..%s, want 25845001..25849899", faixa.CEPIni, faixa.CEPFim) - } - if faixa.IBGE != "3300225" || faixa.Estado != "Rio de Janeiro" || faixa.Regiao != "Sudeste" { - t.Fatalf("faixa lost fields: %+v", faixa) - } -} - -func TestNotFoundWithoutFaixaIsUntouched(t *testing.T) { - f := newFakeAPI(t, http.StatusNotFound, plain404) - _, err := newClient(t, f).Lookup(context.Background(), "99999999") - - if !errors.Is(err, cepis.ErrCEPNotFound) { - t.Fatalf("err = %v, want a not-found error", err) - } - if faixa := FaixaOf(err); faixa != nil { - t.Fatalf("invented a municipality: %+v", faixa) - } - var notFound *NotFoundError - if errors.As(err, ¬Found) { - t.Fatal("a 404 without a faixa should come back as the library's own error") - } -} - -// A faixa the bot cannot trust must cost nothing: the reply falls back to the -// plain one, and never to a panic or an unanswered message. -func TestMalformedFaixaFallsBack(t *testing.T) { - bodies := map[string]string{ - "empty body": ``, - "not JSON": `<html>captive portal</html>`, - "truncated": `{"erro":"cep_nao_encontrado","faixa":{"localidade":"Areal"`, - "faixa is null": `{"erro":"cep_nao_encontrado","faixa":null}`, - "faixa is a string": `{"erro":"cep_nao_encontrado","faixa":"Areal/RJ"}`, - "faixa is an array": `{"erro":"cep_nao_encontrado","faixa":[{"localidade":"Areal"}]}`, - "no localidade": `{"faixa":{"uf":"RJ","cep_ini":"25845001","cep_fim":"25849899"}}`, - "blank localidade": `{"faixa":{"localidade":" ","uf":"RJ"}}`, - "ceps is a string": `{"faixa":{"localidade":"Areal","uf":"RJ","ceps":"118"}}`, - "whole body is null": `null`, - "whole body is array": `[]`, - } - - for name, body := range bodies { - t.Run(name, func(t *testing.T) { - f := newFakeAPI(t, http.StatusNotFound, body) - _, err := newClient(t, f).Lookup(context.Background(), "25845000") - if !errors.Is(err, cepis.ErrCEPNotFound) { - t.Fatalf("err = %v, want a not-found error", err) - } - if faixa := FaixaOf(err); faixa != nil { - t.Fatalf("trusted a broken faixa: %+v", faixa) - } - }) - } -} - -// A negative count is nonsense the formatter would print verbatim, so it is -// flattened to "unknown" while the municipality itself is kept. -func TestNegativeCountIsDropped(t *testing.T) { - f := newFakeAPI(t, http.StatusNotFound, `{"faixa":{"localidade":"Areal","uf":"RJ","ceps":-3}}`) - _, err := newClient(t, f).Lookup(context.Background(), "25845000") - - faixa := FaixaOf(err) - if faixa == nil { - t.Fatal("the municipality should have survived a bad count") - } - if faixa.CEPs != 0 { - t.Fatalf("ceps = %d, want 0", faixa.CEPs) - } -} - -func TestSuccessCostsOneRequest(t *testing.T) { - f := newFakeAPI(t, http.StatusOK, address200) - address, err := newClient(t, f).Lookup(context.Background(), "01001000") - - if err != nil { - t.Fatalf("Lookup: %v", err) - } - if address.Localidade != "São Paulo" { - t.Fatalf("localidade = %q", address.Localidade) - } - if got := f.requests.Load(); got != 1 { - t.Fatalf("the API saw %d requests for a hit, want 1", got) - } -} - -func TestOtherFailuresAskOnlyOnce(t *testing.T) { - f := newFakeAPI(t, http.StatusInternalServerError, `{"erro":"interno"}`) - _, err := newClient(t, f).Lookup(context.Background(), "50000000") - - if !errors.Is(err, cepis.ErrServer) { - t.Fatalf("err = %v, want a server error", err) - } - if got := f.requests.Load(); got != 1 { - t.Fatalf("the API saw %d requests for a 500, want 1", got) - } -} - -// The faixa is worth one short wait and no more. A slow API still gets an -// answer out to the user, without the municipality. -func TestSlowFaixaIsAbandoned(t *testing.T) { - f := newFakeAPI(t, http.StatusNotFound, areal404) - client := newClient(t, f, WithTimeout(20*time.Millisecond)) - f.delay, f.delayFrom = 500*time.Millisecond, 2 - - started := time.Now() - _, err := client.Lookup(context.Background(), "25845000") - elapsed := time.Since(started) - - if !errors.Is(err, cepis.ErrCEPNotFound) { - t.Fatalf("err = %v, want a not-found error", err) - } - if faixa := FaixaOf(err); faixa != nil { - t.Fatalf("used a faixa that arrived too late: %+v", faixa) - } - if elapsed > 400*time.Millisecond { - t.Fatalf("waited %v for the faixa, want it abandoned quickly", elapsed) - } -} - -// A CEP that stopped being a 404 between the two requests is not a -// municipality: the second answer is ignored rather than mixed into the first. -func TestFaixaIgnoresANonNotFoundSecondAnswer(t *testing.T) { - var count atomic.Int64 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if count.Add(1) == 1 { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(plain404)) - return - } - w.WriteHeader(http.StatusOK) - w.Write([]byte(address200)) - })) - t.Cleanup(server.Close) - - api, err := cepis.New(cepis.WithBaseURL(server.URL), cepis.WithMaxAttempts(1)) - if err != nil { - t.Fatalf("cepis.New: %v", err) - } - - _, err = New(api).Lookup(context.Background(), "25845000") - if !errors.Is(err, cepis.ErrCEPNotFound) { - t.Fatalf("err = %v, want a not-found error", err) - } - if faixa := FaixaOf(err); faixa != nil { - t.Fatalf("read a municipality out of a 200: %+v", faixa) - } -} - -func TestFaixaOfAcceptsAnything(t *testing.T) { - for name, err := range map[string]error{ - "nil": nil, - "unrelated": errors.New("boom"), - "not found": cepis.ErrCEPNotFound, - "no faixa": &NotFoundError{Err: cepis.ErrCEPNotFound}, - "wrapped": &NotFoundError{Faixa: &Faixa{Localidade: "Areal", UF: "RJ"}, Err: cepis.ErrCEPNotFound}, - "empty type": &NotFoundError{}, - } { - if name == "wrapped" { - if got := FaixaOf(err); got == nil || got.Place() != "Areal/RJ" { - t.Fatalf("%s: FaixaOf = %+v", name, got) - } - continue - } - if got := FaixaOf(err); got != nil { - t.Fatalf("%s: FaixaOf = %+v, want nil", name, got) - } - } -} - -// The zero value still matches the sentinel, so no construction of this type -// can ever produce an error the bot fails to recognise as a not-found. -func TestEmptyNotFoundErrorStillMatches(t *testing.T) { - var err error = &NotFoundError{} - if !errors.Is(err, cepis.ErrCEPNotFound) { - t.Fatal("an empty NotFoundError should still be a not-found") - } - if err.Error() == "" { - t.Fatal("an empty NotFoundError should still say something") - } -} - -func TestPlaceWithoutUF(t *testing.T) { - faixa := &Faixa{Localidade: "Areal"} - if faixa.Place() != "Areal" { - t.Fatalf("place = %q, want Areal", faixa.Place()) - } - var missing *Faixa - if missing.Place() != "" { - t.Fatalf("place = %q, want empty", missing.Place()) - } -} |