summaryrefslogtreecommitdiffstats
path: root/internal/app/fakes_test.go
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 15:18:21 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 15:18:21 -0300
commit3ca1d2c4370e8e79aba3940d5c8fe115960b75a8 (patch)
treeaa964675c588b80e4f81182d34130ba2d7d984e1 /internal/app/fakes_test.go
downloadtelegram-0.1.0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'internal/app/fakes_test.go')
-rw-r--r--internal/app/fakes_test.go285
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)
+}