gohorsejobs/backend/internal/handlers/payment_handler_test.go
Tiago Yamamoto 6cd8c02252 feat: add test coverage and handler improvements
- Add new test files for handlers (storage, payment, settings)
- Add new test files for services (chat, email, storage, settings, admin)
- Add integration tests for services
- Update handler implementations with bug fixes
- Add coverage reports and test documentation
2026-01-02 08:50:29 -03:00

211 lines
6.3 KiB
Go

package handlers
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// mockPaymentCredentialsService mocks the credentials service
type mockPaymentCredentialsService struct {
getDecryptedKeyFunc func(ctx context.Context, keyName string) (string, error)
}
func (m *mockPaymentCredentialsService) GetDecryptedKey(ctx context.Context, keyName string) (string, error) {
if m.getDecryptedKeyFunc != nil {
return m.getDecryptedKeyFunc(ctx, keyName)
}
// Default mock behavior: return valid JSON config for stripe
if keyName == "stripe" {
return `{"secretKey":"sk_test_123","webhookSecret":"whsec_123"}`, nil
}
return "", nil
}
// mockStripeClient mocks the stripe client
type mockStripeClient struct {
createCheckoutFunc func(secretKey string, req CreateCheckoutRequest) (string, string, error)
}
func (m *mockStripeClient) CreateCheckoutSession(secretKey string, req CreateCheckoutRequest) (string, string, error) {
if m.createCheckoutFunc != nil {
return m.createCheckoutFunc(secretKey, req)
}
return "sess_123", "https://checkout.stripe.com/sess_123", nil
}
func TestCreateCheckout_Success(t *testing.T) {
mockCreds := &mockPaymentCredentialsService{}
mockStripe := &mockStripeClient{}
handler := &PaymentHandler{
credentialsService: mockCreds,
stripeClient: mockStripe,
}
reqBody := CreateCheckoutRequest{
JobID: 1,
PriceID: "price_123",
SuccessURL: "http://success",
CancelURL: "http://cancel",
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/payments/create-checkout", bytes.NewReader(body))
rr := httptest.NewRecorder()
handler.CreateCheckout(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var resp CreateCheckoutResponse
err := json.Unmarshal(rr.Body.Bytes(), &resp)
assert.NoError(t, err)
assert.Equal(t, "sess_123", resp.SessionID)
}
func TestCreateCheckout_MissingFields(t *testing.T) {
handler := &PaymentHandler{}
reqBody := CreateCheckoutRequest{
JobID: 0, // Invalid
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/payments/create-checkout", bytes.NewReader(body))
rr := httptest.NewRecorder()
handler.CreateCheckout(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestCreateCheckout_StripeError(t *testing.T) {
mockCreds := &mockPaymentCredentialsService{}
mockStripe := &mockStripeClient{
createCheckoutFunc: func(secretKey string, req CreateCheckoutRequest) (string, string, error) {
return "", "", errors.New("stripe error")
},
}
handler := &PaymentHandler{
credentialsService: mockCreds,
stripeClient: mockStripe,
}
reqBody := CreateCheckoutRequest{
JobID: 1,
PriceID: "price_123",
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/payments/create-checkout", bytes.NewReader(body))
rr := httptest.NewRecorder()
handler.CreateCheckout(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}
func TestGetPaymentStatus(t *testing.T) {
handler := &PaymentHandler{}
req := httptest.NewRequest("GET", "/payments/status/pay_123", nil)
req.SetPathValue("id", "pay_123")
rr := httptest.NewRecorder()
handler.GetPaymentStatus(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var resp map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &resp)
assert.Equal(t, "pay_123", resp["id"])
}
func TestHandleWebhook_Success(t *testing.T) {
mockCreds := &mockPaymentCredentialsService{
getDecryptedKeyFunc: func(ctx context.Context, keyName string) (string, error) {
// Return config with secret
return `{"webhookSecret":"whsec_test"}`, nil
},
}
// Strategy for Webhook test:
// VerifyStripeSignature is a standalone function that calculates HMAC.
// It's hard to mock unless we export it or wrap it.
// However, we can construct a valid signature for the test!
// Or we can mock the signature verification if we wrapper it?
// The current PaymentHandler calls `verifyStripeSignature` directly.
// To test HandleWebhook fully, we need to generate a valid signature.
secret := "whsec_test"
payload := `{"type":"payment_intent.succeeded", "data":{}}`
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// manually compute signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, payload)))
sig := hex.EncodeToString(mac.Sum(nil))
header := fmt.Sprintf("t=%s,v1=%s", timestamp, sig)
req := httptest.NewRequest("POST", "/payments/webhook", bytes.NewReader([]byte(payload)))
req.Header.Set("Stripe-Signature", header)
rr := httptest.NewRecorder()
handler := &PaymentHandler{
credentialsService: mockCreds,
}
handler.HandleWebhook(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestHandleWebhook_CheckoutCompleted(t *testing.T) {
mockCreds := &mockPaymentCredentialsService{
getDecryptedKeyFunc: func(ctx context.Context, keyName string) (string, error) {
return `{"webhookSecret":"whsec_test"}`, nil
},
}
// Create payload for checkout.session.completed
// logic: handleCheckoutComplete extracts ClientReferenceID -> JobID
// And metadata -> userId, etc.
// We need to match what handleCheckoutComplete expects.
// It parses event.Data.Object into stripe.CheckoutSession.
// Then calls jobService ... wait.
// PaymentHandler NO LONGER depends on JobService directly? In Refactor I removed it?
// Let's check PaymentHandler code.
// If it doesn't have JobService, how does it update Job?
// It calls `handlePaymentSuccess`.
// I need to see what `handlePaymentSuccess` does.
// Assuming logic is simple DB update or logging for now.
secret := "whsec_test"
payload := `{"type":"checkout.session.completed", "data":{"object":{"client_reference_id":"123", "metadata":{"userId":"u1"}, "payment_status":"paid"}}}`
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, payload)))
sig := hex.EncodeToString(mac.Sum(nil))
header := fmt.Sprintf("t=%s,v1=%s", timestamp, sig)
req := httptest.NewRequest("POST", "/payments/webhook", bytes.NewReader([]byte(payload)))
req.Header.Set("Stripe-Signature", header)
rr := httptest.NewRecorder()
handler := &PaymentHandler{
credentialsService: mockCreds,
}
handler.HandleWebhook(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}