feat: sync bot keyboards and callbacks
Sync telesrv b96f2dd (feat(bot): complete keyboards callbacks and durable delivery). Skipped private docs and preserved public README files per sync rules; normalized the appearance seed log label for public naming.
This commit is contained in:
parent
0c99ae0a9d
commit
bf965f610c
80 changed files with 7212 additions and 349 deletions
127
internal/botapi/webhook_test.go
Normal file
127
internal/botapi/webhook_test.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package botapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type recordingWebhookGateway struct {
|
||||
*fakeBotAPIGateway
|
||||
mu sync.Mutex
|
||||
failure string
|
||||
failureNext time.Time
|
||||
successNext time.Time
|
||||
recordedOwner string
|
||||
}
|
||||
|
||||
func (g *recordingWebhookGateway) RecordBotAPIWebhookFailure(_ context.Context, _ int64, owner string, next time.Time, message string) error {
|
||||
g.mu.Lock()
|
||||
g.recordedOwner, g.failure, g.failureNext = owner, message, next
|
||||
g.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *recordingWebhookGateway) RecordBotAPIWebhookSuccess(_ context.Context, _ int64, owner string, next time.Time) error {
|
||||
g.mu.Lock()
|
||||
g.recordedOwner, g.successNext = owner, next
|
||||
g.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func webhookEvents(ids ...int) []domain.UpdateEvent {
|
||||
out := make([]domain.UpdateEvent, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
out = append(out, domain.UpdateEvent{
|
||||
UserID: 1001, Type: domain.UpdateEventNewMessage, Pts: id, Date: 1700000000 + id,
|
||||
Message: domain.Message{
|
||||
ID: id, OwnerUserID: 1001, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2001},
|
||||
From: domain.Peer{Type: domain.PeerTypeUser, ID: 2001}, Date: 1700000000 + id, Body: "message", Out: false,
|
||||
},
|
||||
Users: []domain.User{{ID: 2001, FirstName: "Alice"}},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestWebhookDispatcherPostsInParallelWithSecretAndConfirmsContiguousBatch(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
received := make(map[int]bool)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("X-Telegram-Bot-Api-Secret-Token"); got != "secret_1" {
|
||||
t.Errorf("secret header = %q", got)
|
||||
}
|
||||
var update struct {
|
||||
UpdateID int `json:"update_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
t.Errorf("decode webhook: %v", err)
|
||||
}
|
||||
mu.Lock()
|
||||
received[update.UpdateID] = true
|
||||
mu.Unlock()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
base := &fakeBotAPIGateway{
|
||||
updates: webhookEvents(11, 12, 13),
|
||||
webhook: domain.BotAPIWebhook{BotUserID: 1001, URL: server.URL, SecretToken: "secret_1", MaxConnections: 3},
|
||||
webhookFound: true,
|
||||
}
|
||||
gateway := &recordingWebhookGateway{fakeBotAPIGateway: base}
|
||||
d := &webhookDispatcher{control: gateway, gateway: gateway, client: server.Client(), logger: zap.NewNop(), botSem: make(chan struct{}, 1), httpSem: make(chan struct{}, 8)}
|
||||
d.deliver(context.Background(), base.webhook)
|
||||
|
||||
mu.Lock()
|
||||
count := len(received)
|
||||
mu.Unlock()
|
||||
if count != 3 || base.webhookConfirmed != 13 {
|
||||
t.Fatalf("received=%v confirmed=%d", received, base.webhookConfirmed)
|
||||
}
|
||||
gateway.mu.Lock()
|
||||
successNext, failure := gateway.successNext, gateway.failure
|
||||
gateway.mu.Unlock()
|
||||
if !successNext.After(time.Now().Add(30*time.Minute)) || failure != "" {
|
||||
t.Fatalf("success next=%v failure=%q", successNext, failure)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookDispatcherOnlyConfirmsSuccessfulPrefixAndSchedulesRetry(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var update struct {
|
||||
UpdateID int `json:"update_id"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&update)
|
||||
if update.UpdateID == 22 {
|
||||
http.Error(w, "retry", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
base := &fakeBotAPIGateway{
|
||||
updates: webhookEvents(21, 22, 23),
|
||||
webhook: domain.BotAPIWebhook{BotUserID: 1001, URL: server.URL, MaxConnections: 3},
|
||||
webhookFound: true,
|
||||
}
|
||||
gateway := &recordingWebhookGateway{fakeBotAPIGateway: base}
|
||||
d := &webhookDispatcher{control: gateway, gateway: gateway, client: server.Client(), logger: zap.NewNop(), botSem: make(chan struct{}, 1), httpSem: make(chan struct{}, 8)}
|
||||
d.deliver(context.Background(), base.webhook)
|
||||
|
||||
gateway.mu.Lock()
|
||||
failure, retryAt := gateway.failure, gateway.failureNext
|
||||
gateway.mu.Unlock()
|
||||
if base.webhookConfirmed != 21 || failure != "webhook returned HTTP 503" || !retryAt.After(time.Now()) {
|
||||
t.Fatalf("confirmed=%d failure=%q retry=%v", base.webhookConfirmed, failure, retryAt)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue