diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 15:18:21 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 15:18:21 -0300 |
| commit | 3ca1d2c4370e8e79aba3940d5c8fe115960b75a8 (patch) | |
| tree | aa964675c588b80e4f81182d34130ba2d7d984e1 /internal/app/fakes_test.go | |
| download | telegram-507b9f5009ff8720e0ea9a43c2f6bf888fd53ddb.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'internal/app/fakes_test.go')
| -rw-r--r-- | internal/app/fakes_test.go | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/internal/app/fakes_test.go b/internal/app/fakes_test.go new file mode 100644 index 0000000..011d4af --- /dev/null +++ b/internal/app/fakes_test.go @@ -0,0 +1,285 @@ +package app + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "cep.is/telegrambot/internal/telegram" +) + +const fakeToken = "424242:not-a-real-token-generated-for-tests" + +const fakeUsername = "cepisbot" + +type cannedFailure struct { + status int + code int + description string + retryAfter int +} + +// fakeTelegram is a real HTTP server speaking the Bot API, including the +// confirmation semantics of getUpdates: an update stays pending until a later +// call arrives with a higher offset. Without that, an offset bug in the poller +// would go unnoticed. +type fakeTelegram struct { + server *httptest.Server + username string + + mu sync.Mutex + pending []telegram.Update + offsets []int64 + sent []telegram.SendMessageRequest + inline []telegram.AnswerInlineQueryRequest + sendFailures []cannedFailure + getMeCalls int +} + +func newFakeTelegram(t *testing.T) *fakeTelegram { + t.Helper() + f := &fakeTelegram{username: fakeUsername} + f.server = httptest.NewServer(f) + t.Cleanup(f.server.Close) + return f +} + +func (f *fakeTelegram) URL() string { return f.server.URL } + +func (f *fakeTelegram) ServeHTTP(w http.ResponseWriter, r *http.Request) { + prefix, method, ok := strings.Cut(strings.TrimPrefix(r.URL.Path, "/"), "/") + if !ok || prefix != "bot"+fakeToken { + writeEnvelope(w, http.StatusUnauthorized, envelope{ + OK: false, ErrorCode: 401, Description: "Unauthorized", + }) + return + } + + switch method { + case "getMe": + f.mu.Lock() + f.getMeCalls++ + f.mu.Unlock() + writeResult(w, telegram.User{ID: 42, IsBot: true, FirstName: "cep.is", Username: f.username}) + case "getUpdates": + f.getUpdates(w, r) + case "sendMessage": + f.sendMessage(w, r) + case "answerInlineQuery": + var req telegram.AnswerInlineQueryRequest + decode(r, &req) + f.mu.Lock() + f.inline = append(f.inline, req) + f.mu.Unlock() + writeResult(w, true) + default: + writeEnvelope(w, http.StatusNotFound, envelope{ + OK: false, ErrorCode: 404, Description: "Not Found: method not found", + }) + } +} + +func (f *fakeTelegram) getUpdates(w http.ResponseWriter, r *http.Request) { + var req telegram.GetUpdatesRequest + decode(r, &req) + + f.mu.Lock() + f.offsets = append(f.offsets, req.Offset) + if req.Offset > 0 { + kept := f.pending[:0] + for _, update := range f.pending { + if update.UpdateID >= req.Offset { + kept = append(kept, update) + } + } + f.pending = kept + } + f.mu.Unlock() + + deadline := time.Now().Add(time.Duration(req.Timeout) * time.Second) + for { + f.mu.Lock() + out := append([]telegram.Update(nil), f.pending...) + f.mu.Unlock() + + if len(out) > 0 || time.Now().After(deadline) { + if req.Limit > 0 && len(out) > req.Limit { + out = out[:req.Limit] + } + writeResult(w, out) + return + } + select { + case <-r.Context().Done(): + writeResult(w, []telegram.Update{}) + return + case <-time.After(5 * time.Millisecond): + } + } +} + +func (f *fakeTelegram) sendMessage(w http.ResponseWriter, r *http.Request) { + var req telegram.SendMessageRequest + decode(r, &req) + + f.mu.Lock() + if len(f.sendFailures) > 0 { + failure := f.sendFailures[0] + f.sendFailures = f.sendFailures[1:] + f.mu.Unlock() + body := envelope{OK: false, ErrorCode: failure.code, Description: failure.description} + if failure.retryAfter > 0 { + body.Parameters = &telegram.ResponseParameters{RetryAfter: failure.retryAfter} + } + writeEnvelope(w, failure.status, body) + return + } + f.sent = append(f.sent, req) + f.mu.Unlock() + + writeResult(w, map[string]any{"message_id": 1}) +} + +func (f *fakeTelegram) push(updates ...telegram.Update) { + f.mu.Lock() + defer f.mu.Unlock() + f.pending = append(f.pending, updates...) +} + +func (f *fakeTelegram) failNextSends(failures ...cannedFailure) { + f.mu.Lock() + defer f.mu.Unlock() + f.sendFailures = append(f.sendFailures, failures...) +} + +func (f *fakeTelegram) sentMessages() []telegram.SendMessageRequest { + f.mu.Lock() + defer f.mu.Unlock() + return append([]telegram.SendMessageRequest(nil), f.sent...) +} + +func (f *fakeTelegram) inlineAnswers() []telegram.AnswerInlineQueryRequest { + f.mu.Lock() + defer f.mu.Unlock() + return append([]telegram.AnswerInlineQueryRequest(nil), f.inline...) +} + +func (f *fakeTelegram) requestedOffsets() []int64 { + f.mu.Lock() + defer f.mu.Unlock() + return append([]int64(nil), f.offsets...) +} + +func (f *fakeTelegram) pendingCount() int { + f.mu.Lock() + defer f.mu.Unlock() + return len(f.pending) +} + +// 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. +type fakeCEPIS struct { + server *httptest.Server + + mu sync.Mutex + bodies map[string]string + statuses map[string]int + requests int +} + +func newFakeCEPIS(t *testing.T) *fakeCEPIS { + t.Helper() + f := &fakeCEPIS{ + bodies: map[string]string{ + "01001000": `{"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"}}`, + "76200000": `{"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"}}`, + "29902000": `{"cep":"29902000","logradouro":"Rua Sem Coordenada","complemento":"",` + + `"unidade":"","bairro":"Centro","localidade":"Linhares","uf":"ES","estado":"Espírito Santo",` + + `"regiao":"Sudeste","ibge":"3203205","gia":"","ddd":"27","siafi":"5665"}`, + }, + statuses: map[string]int{ + "50000000": http.StatusInternalServerError, + }, + } + f.server = httptest.NewServer(http.HandlerFunc(f.handle)) + t.Cleanup(f.server.Close) + return f +} + +func (f *fakeCEPIS) URL() string { return f.server.URL } + +func (f *fakeCEPIS) handle(w http.ResponseWriter, r *http.Request) { + f.mu.Lock() + f.requests++ + cep := strings.TrimPrefix(r.URL.Path, "/") + status, forced := f.statuses[cep] + body, known := f.bodies[cep] + f.mu.Unlock() + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + switch { + case forced: + w.WriteHeader(status) + w.Write([]byte(`{"erro":"interno","mensagem":"falha interna"}`)) + case known: + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + case len(cep) != 8: + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"erro":"cep_invalido","mensagem":"CEP deve ter 8 dígitos"}`)) + default: + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}`)) + } +} + +func (f *fakeCEPIS) requestCount() int { + f.mu.Lock() + defer f.mu.Unlock() + return f.requests +} + +type envelope struct { + OK bool `json:"ok"` + Result any `json:"result,omitempty"` + ErrorCode int `json:"error_code,omitempty"` + Description string `json:"description,omitempty"` + Parameters *telegram.ResponseParameters `json:"parameters,omitempty"` +} + +func writeResult(w http.ResponseWriter, result any) { + writeEnvelope(w, http.StatusOK, envelope{OK: true, Result: result}) +} + +func writeEnvelope(w http.ResponseWriter, status int, body envelope) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(body) +} + +func decode(r *http.Request, out any) { + json.NewDecoder(r.Body).Decode(out) +} + +func waitFor(t *testing.T, what string, timeout time.Duration, cond func() bool) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if cond() { + return + } + time.Sleep(5 * time.Millisecond) + } + t.Fatalf("timed out waiting for %s", what) +} |