package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) // TestWebhookEventTypes tests the event types we handle func TestWebhookEventTypes(t *testing.T) { eventTypes := []struct { eventType string shouldHandle bool }{ {"checkout.session.completed", true}, {"customer.subscription.created", true}, {"customer.subscription.updated", true}, {"customer.subscription.deleted", true}, {"invoice.paid", true}, {"invoice.payment_failed", true}, {"customer.created", true}, // Handled but just logged {"unknown.event.type", false}, } for _, tt := range eventTypes { t.Run(tt.eventType, func(t *testing.T) { if tt.eventType == "" { t.Error("Event type should not be empty") } }) } } // TestWebhookRequest_MissingSignature tests handling of missing signature func TestWebhookRequest_MissingSignature(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) // Create request without Stripe-Signature header body := []byte(`{"id": "evt_test_123", "type": "test.event"}`) c.Request = httptest.NewRequest("POST", "/webhook", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") // Note: No Stripe-Signature header // Simulate the check we do in the handler sigHeader := c.GetHeader("Stripe-Signature") if sigHeader == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing signature"}) } if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for missing signature, got %d", w.Code) } var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) if err != nil { t.Fatalf("Failed to parse response: %v", err) } if response["error"] != "missing signature" { t.Errorf("Expected 'missing signature' error, got '%v'", response["error"]) } } // TestWebhookRequest_EmptyBody tests handling of empty request body func TestWebhookRequest_EmptyBody(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) // Create request with empty body c.Request = httptest.NewRequest("POST", "/webhook", bytes.NewReader([]byte{})) c.Request.Header.Set("Content-Type", "application/json") c.Request.Header.Set("Stripe-Signature", "t=123,v1=signature") // Read the body body := make([]byte, 0) // Simulate empty body handling if len(body) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "empty body"}) } if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for empty body, got %d", w.Code) } } // TestWebhookIdempotency tests idempotency behavior func TestWebhookIdempotency(t *testing.T) { // Test that the same event ID should not be processed twice eventID := "evt_test_123456789" // Simulate event tracking processedEvents := make(map[string]bool) // First time - should process if !processedEvents[eventID] { processedEvents[eventID] = true } // Second time - should skip alreadyProcessed := processedEvents[eventID] if !alreadyProcessed { t.Error("Event should be marked as processed") } } // TestWebhookResponse_Processed tests successful webhook response func TestWebhookResponse_Processed(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.JSON(http.StatusOK, gin.H{"status": "processed"}) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) if err != nil { t.Fatalf("Failed to parse response: %v", err) } if response["status"] != "processed" { t.Errorf("Expected status 'processed', got '%v'", response["status"]) } } // TestWebhookResponse_AlreadyProcessed tests idempotent response func TestWebhookResponse_AlreadyProcessed(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.JSON(http.StatusOK, gin.H{"status": "already_processed"}) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) if err != nil { t.Fatalf("Failed to parse response: %v", err) } if response["status"] != "already_processed" { t.Errorf("Expected status 'already_processed', got '%v'", response["status"]) } } // TestWebhookResponse_InternalError tests error response func TestWebhookResponse_InternalError(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.JSON(http.StatusInternalServerError, gin.H{"error": "handler error"}) if w.Code != http.StatusInternalServerError { t.Errorf("Expected status 500, got %d", w.Code) } var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) if err != nil { t.Fatalf("Failed to parse response: %v", err) } if response["error"] != "handler error" { t.Errorf("Expected 'handler error', got '%v'", response["error"]) } } // TestWebhookResponse_InvalidSignature tests signature verification failure func TestWebhookResponse_InvalidSignature(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"}) if w.Code != http.StatusUnauthorized { t.Errorf("Expected status 401, got %d", w.Code) } var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) if err != nil { t.Fatalf("Failed to parse response: %v", err) } if response["error"] != "invalid signature" { t.Errorf("Expected 'invalid signature', got '%v'", response["error"]) } } // TestCheckoutSessionCompleted_EventStructure tests the event data structure func TestCheckoutSessionCompleted_EventStructure(t *testing.T) { // Test the expected structure of a checkout.session.completed event eventData := map[string]interface{}{ "id": "cs_test_123", "customer": "cus_test_456", "subscription": "sub_test_789", "mode": "subscription", "payment_status": "paid", "status": "complete", "metadata": map[string]interface{}{ "user_id": "550e8400-e29b-41d4-a716-446655440000", "plan_id": "standard", }, } data, err := json.Marshal(eventData) if err != nil { t.Fatalf("Failed to marshal event data: %v", err) } var decoded map[string]interface{} err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal event data: %v", err) } // Verify required fields if decoded["customer"] == nil { t.Error("Event should have 'customer' field") } if decoded["subscription"] == nil { t.Error("Event should have 'subscription' field") } metadata, ok := decoded["metadata"].(map[string]interface{}) if !ok || metadata["user_id"] == nil { t.Error("Event should have 'metadata.user_id' field") } } // TestSubscriptionCreated_EventStructure tests subscription.created event structure func TestSubscriptionCreated_EventStructure(t *testing.T) { eventData := map[string]interface{}{ "id": "sub_test_123", "customer": "cus_test_456", "status": "trialing", "items": map[string]interface{}{ "data": []map[string]interface{}{ { "price": map[string]interface{}{ "id": "price_test_789", "metadata": map[string]interface{}{"plan_id": "standard"}, }, }, }, }, "trial_end": 1735689600, "current_period_end": 1735689600, "metadata": map[string]interface{}{ "user_id": "550e8400-e29b-41d4-a716-446655440000", "plan_id": "standard", }, } data, err := json.Marshal(eventData) if err != nil { t.Fatalf("Failed to marshal event data: %v", err) } var decoded map[string]interface{} err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal event data: %v", err) } // Verify required fields if decoded["status"] != "trialing" { t.Errorf("Expected status 'trialing', got '%v'", decoded["status"]) } } // TestSubscriptionUpdated_StatusTransitions tests subscription status transitions func TestSubscriptionUpdated_StatusTransitions(t *testing.T) { validTransitions := []struct { from string to string }{ {"trialing", "active"}, {"active", "past_due"}, {"past_due", "active"}, {"active", "canceled"}, {"trialing", "canceled"}, } for _, tt := range validTransitions { t.Run(tt.from+"->"+tt.to, func(t *testing.T) { if tt.from == "" || tt.to == "" { t.Error("Status should not be empty") } }) } } // TestInvoicePaid_EventStructure tests invoice.paid event structure func TestInvoicePaid_EventStructure(t *testing.T) { eventData := map[string]interface{}{ "id": "in_test_123", "subscription": "sub_test_456", "customer": "cus_test_789", "status": "paid", "amount_paid": 1990, "currency": "eur", "period_start": 1735689600, "period_end": 1738368000, "hosted_invoice_url": "https://invoice.stripe.com/test", "invoice_pdf": "https://invoice.stripe.com/test.pdf", } data, err := json.Marshal(eventData) if err != nil { t.Fatalf("Failed to marshal event data: %v", err) } var decoded map[string]interface{} err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal event data: %v", err) } // Verify required fields if decoded["status"] != "paid" { t.Errorf("Expected status 'paid', got '%v'", decoded["status"]) } if decoded["subscription"] == nil { t.Error("Event should have 'subscription' field") } } // TestInvoicePaymentFailed_EventStructure tests invoice.payment_failed event structure func TestInvoicePaymentFailed_EventStructure(t *testing.T) { eventData := map[string]interface{}{ "id": "in_test_123", "subscription": "sub_test_456", "customer": "cus_test_789", "status": "open", "attempt_count": 1, "next_payment_attempt": 1735776000, } data, err := json.Marshal(eventData) if err != nil { t.Fatalf("Failed to marshal event data: %v", err) } var decoded map[string]interface{} err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal event data: %v", err) } // Verify fields if decoded["attempt_count"] == nil { t.Error("Event should have 'attempt_count' field") } } // TestSubscriptionDeleted_EventStructure tests subscription.deleted event structure func TestSubscriptionDeleted_EventStructure(t *testing.T) { eventData := map[string]interface{}{ "id": "sub_test_123", "customer": "cus_test_456", "status": "canceled", "ended_at": 1735689600, "canceled_at": 1735689600, } data, err := json.Marshal(eventData) if err != nil { t.Fatalf("Failed to marshal event data: %v", err) } var decoded map[string]interface{} err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal event data: %v", err) } // Verify required fields if decoded["status"] != "canceled" { t.Errorf("Expected status 'canceled', got '%v'", decoded["status"]) } } // TestStripeSignatureFormat tests the Stripe signature header format func TestStripeSignatureFormat(t *testing.T) { // Stripe signature format: t=timestamp,v1=signature validSignatures := []string{ "t=1609459200,v1=abc123def456", "t=1609459200,v1=signature_here,v0=old_signature", } for _, sig := range validSignatures { if len(sig) < 10 { t.Errorf("Signature seems too short: %s", sig) } // Should start with timestamp if sig[:2] != "t=" { t.Errorf("Signature should start with 't=': %s", sig) } } } // TestWebhookEventID_Format tests Stripe event ID format func TestWebhookEventID_Format(t *testing.T) { validEventIDs := []string{ "evt_1234567890abcdef", "evt_test_123456789", "evt_live_987654321", } for _, eventID := range validEventIDs { // Event IDs should start with "evt_" if len(eventID) < 10 || eventID[:4] != "evt_" { t.Errorf("Invalid event ID format: %s", eventID) } } }