Files
miti99bot/internal/telegram/webhook_test.go
T
tiennm99 35ad4cb997 feat(server,modules): bootstrap server and module framework
Implements Phases 02 (partial) and 03 of the go-port-cloud-run plan.
Introduces module framework with per-module KV prefix isolation,
health check endpoint, request timeout protection, and comprehensive
test coverage. Cloud Run deployment deferred to Phase 01.

Security hardening: constant-time secret comparison, cron auth bridge,
and secrets stripped from dependency environment exports. Includes
Dockerfile, GitHub CI workflow (vet + race + build), and integration
tests for module lifecycle.
2026-05-08 23:27:12 +07:00

105 lines
3.1 KiB
Go

package telegram
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-telegram/bot"
)
const testSecret = "super-secret-token"
// validUpdate is a minimal Telegram update payload that decodes cleanly. The
// bot has no handlers registered so ProcessUpdate is a no-op match.
const validUpdate = `{"update_id": 1}`
func mustBot(t *testing.T) *bot.Bot {
t.Helper()
b, err := NewBot("TEST:TOKEN")
if err != nil {
t.Fatalf("NewBot: %v", err)
}
return b
}
func TestWebhookHandler_RejectsNonPost(t *testing.T) {
h := WebhookHandler(mustBot(t), testSecret)
req := httptest.NewRequest(http.MethodGet, "/webhook", nil)
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("status = %d, want 405", rec.Code)
}
}
func TestWebhookHandler_RejectsMissingSecret(t *testing.T) {
h := WebhookHandler(mustBot(t), testSecret)
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(validUpdate))
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want 401", rec.Code)
}
}
func TestWebhookHandler_RejectsWrongSecret(t *testing.T) {
h := WebhookHandler(mustBot(t), testSecret)
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(validUpdate))
req.Header.Set(secretTokenHeader, "wrong")
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want 401", rec.Code)
}
}
func TestWebhookHandler_RejectsWrongSecretSamePrefix(t *testing.T) {
// Locks the constant-time compare: a value sharing a prefix must still
// 401, not silently succeed.
h := WebhookHandler(mustBot(t), testSecret)
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(validUpdate))
req.Header.Set(secretTokenHeader, testSecret[:len(testSecret)-1]+"X")
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want 401", rec.Code)
}
}
func TestWebhookHandler_RejectsMalformedJSON(t *testing.T) {
h := WebhookHandler(mustBot(t), testSecret)
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader("not-json"))
req.Header.Set(secretTokenHeader, testSecret)
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", rec.Code)
}
}
func TestWebhookHandler_RejectsOversizedBody(t *testing.T) {
h := WebhookHandler(mustBot(t), testSecret)
body := bytes.Repeat([]byte("a"), maxWebhookBody+1)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
req.Header.Set(secretTokenHeader, testSecret)
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code == http.StatusOK {
t.Errorf("oversized body should not return 200; got %d", rec.Code)
}
}
func TestWebhookHandler_AcceptsValidUpdate(t *testing.T) {
h := WebhookHandler(mustBot(t), testSecret)
req := httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(validUpdate))
req.Header.Set(secretTokenHeader, testSecret)
rec := httptest.NewRecorder()
h(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
}