Files
Crussell/backend/handlers/webhooks/webhooks_test.go
T
popertotsandSisyphus e4b9003439 refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:43:50 +01:00

251 lines
7.9 KiB
Go

//go:build test
// +build test
package webhooks
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// =============================================================================
// Unit tests — verifySquareSignature (pure function)
// =============================================================================
func TestVerifySquareSignature_ValidSignature(t *testing.T) {
t.Parallel()
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
key := "test-signing-key"
notificationURL := "http://localhost:8080/webhooks/square"
payload := notificationURL + string(body)
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(payload))
expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
if !verifySquareSignature(body, expectedSig, key, notificationURL) {
t.Error("expected valid signature to verify")
}
}
func TestVerifySquareSignature_InvalidSignature(t *testing.T) {
t.Parallel()
body := []byte(`{"type":"payment.updated"}`)
key := "test-signing-key"
notificationURL := "http://localhost:8080/webhooks/square"
if verifySquareSignature(body, "invalid-signature", key, notificationURL) {
t.Error("expected invalid signature to fail")
}
}
func TestVerifySquareSignature_WrongKey(t *testing.T) {
t.Parallel()
body := []byte(`{"type":"payment.updated"}`)
notificationURL := "http://localhost:8080/webhooks/square"
payload := notificationURL + string(body)
mac := hmac.New(sha256.New, []byte("correct-key"))
mac.Write([]byte(payload))
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// Verify with a different key
if verifySquareSignature(body, sig, "wrong-key", notificationURL) {
t.Error("expected wrong key to produce failing verification")
}
}
func TestVerifySquareSignature_EmptyBody(t *testing.T) {
t.Parallel()
key := "test-signing-key"
notificationURL := "http://localhost:8080/webhooks/square"
payload := notificationURL + string([]byte{})
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(payload))
expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
if !verifySquareSignature([]byte{}, expectedSig, key, notificationURL) {
t.Error("expected empty body verification to succeed with matching signature")
}
}
func TestVerifySquareSignature_TamperedBody(t *testing.T) {
t.Parallel()
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
key := "test-signing-key"
notificationURL := "http://localhost:8080/webhooks/square"
payload := notificationURL + string(body)
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(payload))
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// Verify with a tampered body
tamperedBody := []byte(`{"type":"payment.updated","event_id":"evt_2"}`)
if verifySquareSignature(tamperedBody, sig, key, notificationURL) {
t.Error("expected tampered body to fail verification")
}
}
// =============================================================================
// Integration tests — HandleSquareWebhook
// =============================================================================
func makeWebhookRequest(body []byte, signature string, ctx context.Context) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/webhooks/square", bytes.NewReader(body))
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
if signature != "" {
req.Header.Set("x-square-hmacsha256-signature", signature)
}
HandleSquareWebhook(w, req)
return w
}
func TestHandleSquareWebhook_PaymentUpdated(t *testing.T) {
t.Parallel()
event := SquareWebhookEvent{
Type: "payment.updated",
EventID: "evt_payment_1",
CreatedAt: "2025-01-01T00:00:00Z",
Data: json.RawMessage(`{"id":"payment_1"}`),
}
body, _ := json.Marshal(event)
w := makeWebhookRequest(body, "", context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
if w.Body.String() != "ok" {
t.Errorf("expected body 'ok', got %q", w.Body.String())
}
}
func TestHandleSquareWebhook_RefundUpdated(t *testing.T) {
t.Parallel()
event := SquareWebhookEvent{
Type: "refund.updated",
EventID: "evt_refund_1",
CreatedAt: "2025-01-01T00:00:00Z",
Data: json.RawMessage(`{"id":"refund_1"}`),
}
body, _ := json.Marshal(event)
w := makeWebhookRequest(body, "", context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_DisputeCreated(t *testing.T) {
t.Parallel()
event := SquareWebhookEvent{
Type: "dispute.created",
EventID: "evt_dispute_1",
CreatedAt: "2025-01-01T00:00:00Z",
Data: json.RawMessage(`{"id":"dispute_1"}`),
}
body, _ := json.Marshal(event)
w := makeWebhookRequest(body, "", context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200 for dispute.created, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_UnknownEventType(t *testing.T) {
t.Parallel()
event := SquareWebhookEvent{
Type: "invoice.created",
EventID: "evt_unknown_1",
CreatedAt: "2025-01-01T00:00:00Z",
Data: json.RawMessage(`{"id":"inv_1"}`),
}
body, _ := json.Marshal(event)
w := makeWebhookRequest(body, "", context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200 for unknown event type, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_InvalidJSON(t *testing.T) {
t.Parallel()
w := makeWebhookRequest([]byte(`{invalid json}`), "", context.Background())
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for invalid JSON, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_BodyTooLarge(t *testing.T) {
t.Parallel()
// 600KB body exceeds the 512KB limit
largeBody := []byte(strings.Repeat("a", 600*1024))
w := makeWebhookRequest(largeBody, "", context.Background())
if w.Code != http.StatusRequestEntityTooLarge {
t.Errorf("expected 413 for oversized body, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
key := "env-signing-key"
notificationURL := "http://localhost:8080/webhooks/square"
payload := notificationURL + string(body)
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(payload))
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", key)
w := makeWebhookRequest(body, sig, context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200 with valid signature, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_InvalidSignatureWithEnvKey(t *testing.T) {
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "env-signing-key")
w := makeWebhookRequest(body, "bad-signature", context.Background())
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 with invalid signature, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_NoSignatureWhenKeySet(t *testing.T) {
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "env-signing-key")
// No x-square-signature header at all
w := makeWebhookRequest(body, "", context.Background())
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 when signature key is set but header missing, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestHandleSquareWebhook_SignatureSkippedWhenKeyEmpty(t *testing.T) {
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "")
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
// Bad signature but key is empty, so verification should be skipped
w := makeWebhookRequest(body, "some-signature", context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200 when no key configured (dev stub), got %d. body: %s", w.Code, w.Body.String())
}
}