package bot import ( "encoding/json" "errors" "strings" "testing" cepis "cep.is/go" "cep.is/telegrambot/internal/lookup" ) func decodeAddress(t *testing.T, body string) *cepis.Address { t.Helper() var address cepis.Address if err := json.Unmarshal([]byte(body), &address); err != nil { t.Fatalf("decode: %v", err) } return &address } const streetJSON = `{"cep":"01001000","logradouro":"Praça da Sé","complemento":"lado ímpar", "unidade":"","bairro":"Sé","localidade":"São Paulo","uf":"SP","estado":"São Paulo", "regiao":"Sudeste","ibge":"3550308","gia":"1004","ddd":"11","siafi":"7107", "geo":{"lat":-23.567202,"lon":-46.649659,"raio_m":128,"precisao":"logradouro"}}` const municipalityJSON = `{"cep":"76200000","logradouro":"","complemento":"","unidade":"", "bairro":"","localidade":"Iporá","uf":"GO","estado":"Goiás","regiao":"Centro-Oeste", "ibge":"5210000","gia":"","ddd":"64","siafi":"9439", "geo":{"lat":-16.441,"lon":-51.118,"raio_m":18400,"precisao":"localidade"}}` func TestFormatStreetLevelCEP(t *testing.T) { text := FormatAddress(decodeAddress(t, streetJSON)) for _, needed := range []string{"01001-000", "Praça da Sé", "lado ímpar", "Sé — São Paulo/SP", "DDD 11", "128 m"} { if !strings.Contains(text, needed) { t.Fatalf("reply is missing %q:\n%s", needed, text) } } if !strings.Contains(text, "openstreetmap.org/?mlat=-23.567202&mlon=-46.649659") { t.Fatalf("a street-level CEP should link to the point:\n%s", text) } } func TestFormatMunicipalityCEP(t *testing.T) { text := FormatAddress(decodeAddress(t, municipalityJSON)) if strings.Contains(text, "openstreetmap.org") { t.Fatalf("a municipality CEP must not offer a pin:\n%s", text) } for _, needed := range []string{"CEP geral de município", "centroide de Iporá", "18,4 km", "não serve", "Iporá/GO"} { if !strings.Contains(strings.ToLower(text), strings.ToLower(needed)) { t.Fatalf("reply is missing %q:\n%s", needed, text) } } } func TestFormatDiffersByPrecision(t *testing.T) { street := FormatAddress(decodeAddress(t, streetJSON)) municipality := FormatAddress(decodeAddress(t, municipalityJSON)) if street == municipality { t.Fatal("the two precisions produced the same reply") } } func TestFormatWithoutGeo(t *testing.T) { text := FormatAddress(decodeAddress(t, `{"cep":"29902000","logradouro":"Rua X","localidade":"Linhares","uf":"ES"}`)) if !strings.Contains(text, "Sem coordenada") { t.Fatalf("reply should say there is no coordinate:\n%s", text) } if strings.Contains(text, "openstreetmap.org") { t.Fatalf("no coordinate must mean no link:\n%s", text) } } func TestFormatEscapesHTML(t *testing.T) { text := FormatAddress(decodeAddress(t, `{"cep":"12345678","logradouro":"Rua Fake & Cia","localidade":"X","uf":"SP"}`)) if strings.Contains(text, "Fake") { t.Fatalf("address data must be escaped:\n%s", text) } if !strings.Contains(text, "<b>Fake</b> & Cia") { t.Fatalf("escaping is wrong:\n%s", text) } } func TestInlineDescriptionWarnsAboutMunicipality(t *testing.T) { description := InlineDescription(decodeAddress(t, municipalityJSON)) if !strings.Contains(description, "CEP geral do município") { t.Fatalf("description = %q", description) } if street := InlineDescription(decodeAddress(t, streetJSON)); strings.Contains(street, "geral do município") { t.Fatalf("street description = %q", street) } } func TestErrorMessagesAreDistinct(t *testing.T) { cases := map[string]error{ "not found": cepis.ErrCEPNotFound, "invalid": cepis.ErrInvalidCEP, "rate limited": cepis.ErrRateLimited, "server": cepis.ErrServer, "network": cepis.ErrNetwork, "invalid response": cepis.ErrInvalidResponse, "unknown": errors.New("something else"), } seen := make(map[string]string, len(cases)) for name, err := range cases { message := ErrorMessage(err) if message == "" { t.Fatalf("%s produced no message", name) } if other, clash := seen[message]; clash { t.Fatalf("%s and %s share a message: %q", name, other, message) } seen[message] = name } } func notFound(faixa *lookup.Faixa) error { return &lookup.NotFoundError{Faixa: faixa, Err: cepis.ErrCEPNotFound} } var areal = &lookup.Faixa{ Localidade: "Areal", UF: "RJ", Estado: "Rio de Janeiro", Regiao: "Sudeste", IBGE: "3300225", CEPIni: "25845001", CEPFim: "25849899", CEPs: 118, } func TestNotFoundNamesTheMunicipality(t *testing.T) { text := ErrorMessage(notFound(areal)) for _, needed := range []string{ "não encontrado", "Areal/RJ", "118 CEPs", "25845-001", "25849-899", "esparsa", "Confira os oito dígitos", } { if !strings.Contains(text, needed) { t.Fatalf("reply is missing %q:\n%s", needed, text) } } } // The municipality is context on a failure. A reply that looks like a hit would // have someone write the city down as the answer. func TestNotFoundDoesNotLookLikeAnAnswer(t *testing.T) { text := ErrorMessage(notFound(areal)) if strings.Contains(text, "📮") { t.Fatalf("the found-address marker leaked into a failure:\n%s", text) } if !strings.HasPrefix(text, notFoundHead) { t.Fatalf("the reply must open by saying the CEP does not exist:\n%s", text) } if !strings.HasSuffix(text, notFoundAdvice) { t.Fatalf("the reply must close with the usual advice:\n%s", text) } if strings.Contains(text, "25845-000") { t.Fatalf("the reply suggests the CEP that just failed:\n%s", text) } } // Absence of a faixa must be indistinguishable from the day before this // existed, byte for byte. func TestNotFoundWithoutFaixaIsUnchanged(t *testing.T) { const want = "🔍 CEP não encontrado na base dos Correios (DNE).\n" + "Confira os oito dígitos — a base é atualizada a cada quinze dias." for name, err := range map[string]error{ "plain sentinel": cepis.ErrCEPNotFound, "no faixa": notFound(nil), "empty faixa": notFound(&lookup.Faixa{}), } { if got := ErrorMessage(err); got != want { t.Fatalf("%s:\n got %q\nwant %q", name, got, want) } } } func TestNotFoundEscapesTheMunicipality(t *testing.T) { text := ErrorMessage(notFound(&lookup.Faixa{ Localidade: "Santa Rita & Cia", UF: "M&G", CEPIni: "25845001", CEPFim: "25849899", CEPs: 2, })) if strings.Contains(text, "Rita") { t.Fatalf("the municipality name must be escaped:\n%s", text) } if !strings.Contains(text, "Santa <b>Rita</b> & Cia/M&G") { t.Fatalf("escaping is wrong:\n%s", text) } } // Every field of the faixa is optional in practice, and a missing one may not // take the sentence down with it. func TestNotFoundDegradesFieldByField(t *testing.T) { cases := map[string]struct { faixa *lookup.Faixa present []string absent []string }{ "no count": { faixa: &lookup.Faixa{Localidade: "Areal", UF: "RJ", CEPIni: "25845001", CEPFim: "25849899"}, present: []string{"Areal/RJ", "25845-001", "25849-899", "esparsa"}, absent: []string{"CEPs,"}, }, "no range": { faixa: &lookup.Faixa{Localidade: "Areal", UF: "RJ", CEPs: 118}, present: []string{"Areal/RJ", "118 CEPs"}, absent: []string{"", "esparsa"}, }, "only the name": { faixa: &lookup.Faixa{Localidade: "Areal"}, present: []string{"cairia em Areal."}, absent: []string{"", "esparsa", "Areal/"}, }, "single CEP": { faixa: &lookup.Faixa{Localidade: "Vila", UF: "SP", CEPIni: "12345678", CEPFim: "12345678", CEPs: 1}, present: []string{"1 CEP."}, absent: []string{"12345-678"}, }, "unusable range": { faixa: &lookup.Faixa{Localidade: "Areal", UF: "RJ", CEPIni: "abc", CEPFim: "2584", CEPs: 118}, present: []string{"118 CEPs."}, absent: []string{"abc", "2584"}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { text := ErrorMessage(notFound(tc.faixa)) for _, needed := range tc.present { if !strings.Contains(text, needed) { t.Fatalf("missing %q:\n%s", needed, text) } } for _, unwanted := range tc.absent { if strings.Contains(text, unwanted) { t.Fatalf("should not carry %q:\n%s", unwanted, text) } } }) } } func TestInlineErrorDescriptionNamesTheMunicipality(t *testing.T) { if got := inlineErrorDescription(notFound(areal)); got != "Cairia em Areal/RJ — toque para ver" { t.Fatalf("description = %q", got) } if got := inlineErrorDescription(cepis.ErrCEPNotFound); got != "Toque para enviar o motivo" { t.Fatalf("description without a faixa = %q", got) } // Titles and descriptions are plain text: escaping them would show the // entities to the user. got := inlineErrorDescription(notFound(&lookup.Faixa{Localidade: "Santa & Cia", UF: "SP"})) if !strings.Contains(got, "Santa & Cia/SP") { t.Fatalf("description = %q", got) } } func TestHelpMentionsEveryEntryPoint(t *testing.T) { text := HelpMessage("cepisbot") for _, needed := range []string{"01001-000", "/cep 01001000", "@cepisbot 01001000", "https://cep.is"} { if !strings.Contains(text, needed) { t.Fatalf("help is missing %q:\n%s", needed, text) } } }