owpengram-server/cmd/otpwebhook-example/main_test.go

164 lines
4.9 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"sync/atomic"
"testing"
"time"
)
func TestDeliveryAcceptsSignedRequestAndDeduplicatesReplay(t *testing.T) {
now := time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)
var calls atomic.Int32
app := testApplication(now, func(_ context.Context, _ deliveryRequest) (string, error) {
calls.Add(1)
return "provider-message-1", nil
})
body := marshalRequest(t, validRequest(now))
for range 2 {
response := performDelivery(t, app.routes(), body, "otp_test_1", "test-secret", now)
if response.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
var result deliveryResponse
if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if !result.Accepted || result.MessageID != "provider-message-1" {
t.Fatalf("unexpected response: %+v", result)
}
}
if got := calls.Load(); got != 1 {
t.Fatalf("deliver calls = %d, want 1", got)
}
}
func TestDeliveryRejectsIdempotencyConflict(t *testing.T) {
now := time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)
app := testApplication(now, func(_ context.Context, _ deliveryRequest) (string, error) {
return "provider-message-1", nil
})
first := validRequest(now)
response := performDelivery(t, app.routes(), marshalRequest(t, first), first.DeliveryID, "test-secret", now)
if response.Code != http.StatusOK {
t.Fatalf("first status = %d", response.Code)
}
second := first
second.Code = "654321"
response = performDelivery(t, app.routes(), marshalRequest(t, second), second.DeliveryID, "test-secret", now)
if response.Code != http.StatusConflict {
t.Fatalf("conflict status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestDeliveryRejectsInvalidSignature(t *testing.T) {
now := time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)
app := testApplication(now, func(_ context.Context, _ deliveryRequest) (string, error) {
t.Fatal("deliver must not be called")
return "", nil
})
request := validRequest(now)
response := performDelivery(t, app.routes(), marshalRequest(t, request), request.DeliveryID, "wrong-secret", now)
if response.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestDeliveryRejectsExpiredCode(t *testing.T) {
now := time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)
app := testApplication(now, func(_ context.Context, _ deliveryRequest) (string, error) {
t.Fatal("deliver must not be called")
return "", nil
})
request := validRequest(now)
request.ExpiresAt = now.Add(-time.Second)
response := performDelivery(t, app.routes(), marshalRequest(t, request), request.DeliveryID, "test-secret", now)
if response.Code != http.StatusBadRequest {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestDeliveryFailureIsAlsoDeduplicated(t *testing.T) {
now := time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)
var calls atomic.Int32
app := testApplication(now, func(_ context.Context, _ deliveryRequest) (string, error) {
calls.Add(1)
return "", errors.New("downstream outcome unknown")
})
request := validRequest(now)
body := marshalRequest(t, request)
for range 2 {
response := performDelivery(t, app.routes(), body, request.DeliveryID, "test-secret", now)
if response.Code != http.StatusBadGateway {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
}
if got := calls.Load(); got != 1 {
t.Fatalf("deliver calls = %d, want 1", got)
}
}
func testApplication(now time.Time, deliver deliveryFunc) *application {
return newApplication(
"test-secret",
5*time.Minute,
func() time.Time { return now },
deliver,
slog.New(slog.NewTextHandler(io.Discard, nil)),
)
}
func validRequest(now time.Time) deliveryRequest {
return deliveryRequest{
Version: "1",
DeliveryID: "otp_test_1",
Purpose: "login_email",
Channel: "email",
Recipient: "alice@example.test",
Code: "123456",
ExpiresAt: now.Add(5 * time.Minute),
ExpiresIn: 300,
Locale: "zh-CN",
}
}
func marshalRequest(t *testing.T, request deliveryRequest) []byte {
t.Helper()
body, err := json.Marshal(request)
if err != nil {
t.Fatal(err)
}
return body
}
func performDelivery(
t *testing.T,
handler http.Handler,
body []byte,
deliveryID string,
secret string,
now time.Time,
) *httptest.ResponseRecorder {
t.Helper()
timestampText := strconv.FormatInt(now.Unix(), 10)
request := httptest.NewRequest(http.MethodPost, "/v1/otp/deliveries", bytes.NewReader(body))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Idempotency-Key", deliveryID)
request.Header.Set("X-Telesrv-Timestamp", timestampText)
request.Header.Set("X-Telesrv-Signature", signatureFor([]byte(secret), timestampText, body))
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
return response
}