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)
}
}
}