diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:46:40 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:46:40 -0300 |
| commit | 83ce95cf10fd9e9d581362453c558239d37ae06e (patch) | |
| tree | dcaf1e9258e1c55c2df19f6556e15c39eaf622bb /internal/app | |
| parent | 3ca1d2c4370e8e79aba3940d5c8fe115960b75a8 (diff) | |
| download | telegram-0.2.0.tar.gz | |
Name the municipality when the CEP does not existv0.2.0
Answering only "não encontrei" wastes the trip. A CEP that fails is
almost never a random number: it is someone who knows the city and
guessed the rest, most often at the "-000" that 957 municipalities never
received. The API now names that municipality on the 404, so the reply
names it too.
It stays an answer to a failure. No 📮, no address block, and no CEP
suggested from inside the range — the range is sparse (Areal's 118 CEPs
span nearly five thousand numbers), so the reply gives the count and says
so instead of letting the two ends read as a list of valid CEPs.
cep.is/go v0.1.0 discards the 404 body, so internal/lookup reads it with
one extra GET, on that path alone and never inside a retry. Everything
that can go wrong there — a slow answer, broken JSON, a faixa naming no
municipality — falls back to the reply the bot has always given. The
wrapper goes away the day the library exposes the field.
Diffstat (limited to 'internal/app')
| -rw-r--r-- | internal/app/app.go | 11 | ||||
| -rw-r--r-- | internal/app/app_test.go | 50 | ||||
| -rw-r--r-- | internal/app/fakes_test.go | 20 |
3 files changed, 77 insertions, 4 deletions
diff --git a/internal/app/app.go b/internal/app/app.go index 0b78baf..ccb8483 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -15,6 +15,7 @@ import ( "cep.is/telegrambot/internal/bot" "cep.is/telegrambot/internal/config" + "cep.is/telegrambot/internal/lookup" "cep.is/telegrambot/internal/telegram" ) @@ -34,22 +35,28 @@ type App struct { // The whole-call timeout of the cep.is client is the shutdown grace on purpose: // handling one update is what a termination signal waits for, so a lookup must // not be allowed to outlast that budget. +// +// The client goes to the bot wrapped in [lookup.Client], which is what keeps the +// municipality the API names on a 404. The wrapper has its own short timeout, +// deliberately far below the grace: it is a footnote on an answer that is +// already writable. func New(ctx context.Context, cfg config.Config, logger *slog.Logger) (*App, error) { if logger == nil { logger = slog.Default() } - lookups, err := cepis.New( + cepisClient, err := cepis.New( cepis.WithBaseURL(cfg.CepisBaseURL), cepis.WithTimeout(cfg.RequestTimeout), cepis.WithMaxAttempts(cfg.MaxAttempts), cepis.WithBackoff(cfg.BackoffBase, cfg.BackoffMax), cepis.WithTotalTimeout(cfg.ShutdownGrace), - cepis.WithUserAgent("cepis-telegram/0.1.0 (+https://cep.is)"), + cepis.WithUserAgent("cepis-telegram/0.2.0 (+https://cep.is)"), ) if err != nil { return nil, fmt.Errorf("app: cep.is client: %w", err) } + lookups := lookup.New(cepisClient) api, err := telegram.New(cfg.Token, telegram.WithBaseURL(cfg.TelegramAPIBaseURL), diff --git a/internal/app/app_test.go b/internal/app/app_test.go index e770060..d430e11 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -291,6 +291,8 @@ func TestMalformedCEP(t *testing.T) { } } +// A CEP in a numeric band the Correios never handed to any municipality: the +// 404 carries no faixa, and the answer is the one the bot has always given. func TestCEPNotFound(t *testing.T) { tg, api := newFakeTelegram(t), newFakeCEPIS(t) tg.push(privateMessage(1, 555, "99999999")) @@ -300,6 +302,54 @@ func TestCEPNotFound(t *testing.T) { if !strings.Contains(sent[0].Text, "não encontrado") { t.Fatalf("reply = %q, want a not-found message", sent[0].Text) } + if strings.Contains(sent[0].Text, "cairia em") { + t.Fatalf("named a municipality the API did not send: %q", sent[0].Text) + } +} + +// The "-000" guess for Areal/RJ, which never received that number. The reply +// still says the CEP does not exist, and adds where it would have been. +func TestCEPNotFoundNamesTheMunicipality(t *testing.T) { + tg, api := newFakeTelegram(t), newFakeCEPIS(t) + tg.push(privateMessage(1, 555, "25845-000")) + start(t, testConfig(tg, api)) + + sent := waitForMessages(t, tg, 1) + text := sent[0].Text + for _, needed := range []string{ + "não encontrado", "Areal/RJ", "118 CEPs", "25845-001", "25849-899", "esparsa", + } { + if !strings.Contains(text, needed) { + t.Fatalf("reply is missing %q: %q", needed, text) + } + } + if strings.Contains(text, "📮") { + t.Fatalf("a failure must not be formatted as a hit: %q", text) + } + if strings.Contains(text, "25845-000") { + t.Fatalf("the reply hands back the CEP that just failed: %q", text) + } +} + +func TestInlineNotFoundNamesTheMunicipality(t *testing.T) { + tg, api := newFakeTelegram(t), newFakeCEPIS(t) + tg.push(inlineQuery(1, 555, "25845000")) + start(t, testConfig(tg, api)) + + waitFor(t, "inline answer", 10*time.Second, func() bool { return len(tg.inlineAnswers()) >= 1 }) + results := tg.inlineAnswers()[0].Results + if len(results) != 1 { + t.Fatalf("got %d results, want 1", len(results)) + } + if results[0].Title != "CEP não encontrado" { + t.Fatalf("title = %q", results[0].Title) + } + if !strings.Contains(results[0].Description, "Areal/RJ") { + t.Fatalf("description = %q, want the municipality named", results[0].Description) + } + if !strings.Contains(results[0].InputMessageContent.MessageText, "118 CEPs") { + t.Fatalf("message = %q", results[0].InputMessageContent.MessageText) + } } func TestAPIServerError(t *testing.T) { diff --git a/internal/app/fakes_test.go b/internal/app/fakes_test.go index 011d4af..9281a53 100644 --- a/internal/app/fakes_test.go +++ b/internal/app/fakes_test.go @@ -182,12 +182,16 @@ func (f *fakeTelegram) pendingCount() int { // fakeCEPIS is a real HTTP server speaking the cep.is native contract, with // enough of it to exercise every branch the bot has: an address with a street -// coordinate, one that is a whole municipality, a 404, a 400 and a 500. +// coordinate, one that is a whole municipality, a 404 that names the +// municipality the CEP would fall in, a 404 that does not, a 400 and a 500. type fakeCEPIS struct { server *httptest.Server - mu sync.Mutex + mu sync.Mutex + // bodies are the 200s, missing are the 404s that carry a faixa, and + // statuses forces a status for a CEP regardless of the other two. bodies map[string]string + missing map[string]string statuses map[string]int requests int } @@ -208,6 +212,14 @@ func newFakeCEPIS(t *testing.T) *fakeCEPIS { `"unidade":"","bairro":"Centro","localidade":"Linhares","uf":"ES","estado":"Espírito Santo",` + `"regiao":"Sudeste","ibge":"3203205","gia":"","ddd":"27","siafi":"5665"}`, }, + // 25845000 is the CEP the "-000" guess produces for Areal/RJ, which + // is coded street by street and never received that number: the + // municipality exists, the CEP does not. + missing: map[string]string{ + "25845000": `{"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}}`, + }, statuses: map[string]int{ "50000000": http.StatusInternalServerError, }, @@ -225,6 +237,7 @@ func (f *fakeCEPIS) handle(w http.ResponseWriter, r *http.Request) { cep := strings.TrimPrefix(r.URL.Path, "/") status, forced := f.statuses[cep] body, known := f.bodies[cep] + absent, attributed := f.missing[cep] f.mu.Unlock() w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -235,6 +248,9 @@ func (f *fakeCEPIS) handle(w http.ResponseWriter, r *http.Request) { case known: w.WriteHeader(http.StatusOK) w.Write([]byte(body)) + case attributed: + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(absent)) case len(cep) != 8: w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"erro":"cep_invalido","mensagem":"CEP deve ter 8 dígitos"}`)) |