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) }