Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes: - Migrate all handlers from Gin to Echo framework - Add new apperrors, echohelpers, and validator packages - Update middleware for Echo compatibility - Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column) - Add 6 new integration tests: - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks - MultiUserSharing: Complex sharing with user removal - TaskStateTransitions: All state transitions and kanban column changes - DateBoundaryEdgeCases: Threshold boundary testing - CascadeOperations: Residence deletion cascade effects - MultiUserOperations: Shared residence collaboration - Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.) - Fix RemoveUser route param mismatch (userId -> user_id) - Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/config"
|
||||
@@ -93,27 +93,24 @@ type AppleRenewalInfo struct {
|
||||
}
|
||||
|
||||
// HandleAppleWebhook handles POST /api/subscription/webhook/apple/
|
||||
func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c echo.Context) error {
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
log.Printf("Apple Webhook: Failed to read body: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "failed to read request body"})
|
||||
}
|
||||
|
||||
var payload AppleNotificationPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
log.Printf("Apple Webhook: Failed to parse payload: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid payload"})
|
||||
}
|
||||
|
||||
// Decode and verify the signed payload (JWS)
|
||||
notification, err := h.decodeAppleSignedPayload(payload.SignedPayload)
|
||||
if err != nil {
|
||||
log.Printf("Apple Webhook: Failed to decode signed payload: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signed payload"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid signed payload"})
|
||||
}
|
||||
|
||||
log.Printf("Apple Webhook: Received %s (subtype: %s) for bundle %s",
|
||||
@@ -125,8 +122,7 @@ func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
if notification.Data.BundleID != cfg.AppleIAP.BundleID {
|
||||
log.Printf("Apple Webhook: Bundle ID mismatch: got %s, expected %s",
|
||||
notification.Data.BundleID, cfg.AppleIAP.BundleID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bundle ID mismatch"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "bundle ID mismatch"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +130,7 @@ func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
transactionInfo, err := h.decodeAppleTransaction(notification.Data.SignedTransactionInfo)
|
||||
if err != nil {
|
||||
log.Printf("Apple Webhook: Failed to decode transaction: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid transaction info"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid transaction info"})
|
||||
}
|
||||
|
||||
// Decode renewal info if present
|
||||
@@ -151,7 +146,7 @@ func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Always return 200 OK to acknowledge receipt
|
||||
c.JSON(http.StatusOK, gin.H{"status": "received"})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"status": "received"})
|
||||
}
|
||||
|
||||
// decodeAppleSignedPayload decodes and verifies an Apple JWS payload
|
||||
@@ -454,41 +449,36 @@ const (
|
||||
)
|
||||
|
||||
// HandleGoogleWebhook handles POST /api/subscription/webhook/google/
|
||||
func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c *gin.Context) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c echo.Context) error {
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
log.Printf("Google Webhook: Failed to read body: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "failed to read request body"})
|
||||
}
|
||||
|
||||
var notification GoogleNotification
|
||||
if err := json.Unmarshal(body, ¬ification); err != nil {
|
||||
log.Printf("Google Webhook: Failed to parse notification: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid notification"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid notification"})
|
||||
}
|
||||
|
||||
// Decode the base64 data
|
||||
data, err := base64.StdEncoding.DecodeString(notification.Message.Data)
|
||||
if err != nil {
|
||||
log.Printf("Google Webhook: Failed to decode message data: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message data"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid message data"})
|
||||
}
|
||||
|
||||
var devNotification GoogleDeveloperNotification
|
||||
if err := json.Unmarshal(data, &devNotification); err != nil {
|
||||
log.Printf("Google Webhook: Failed to parse developer notification: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid developer notification"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid developer notification"})
|
||||
}
|
||||
|
||||
// Handle test notification
|
||||
if devNotification.TestNotification != nil {
|
||||
log.Printf("Google Webhook: Received test notification")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "test received"})
|
||||
return
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"status": "test received"})
|
||||
}
|
||||
|
||||
// Verify package name
|
||||
@@ -497,8 +487,7 @@ func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c *gin.Context) {
|
||||
if devNotification.PackageName != cfg.GoogleIAP.PackageName {
|
||||
log.Printf("Google Webhook: Package name mismatch: got %s, expected %s",
|
||||
devNotification.PackageName, cfg.GoogleIAP.PackageName)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "package name mismatch"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "package name mismatch"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +500,7 @@ func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Acknowledge the message
|
||||
c.JSON(http.StatusOK, gin.H{"status": "received"})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"status": "received"})
|
||||
}
|
||||
|
||||
// processGoogleSubscriptionNotification handles Google subscription events
|
||||
@@ -736,7 +725,7 @@ func (h *SubscriptionWebhookHandler) VerifyAppleSignature(signedPayload string)
|
||||
}
|
||||
|
||||
// VerifyGooglePubSubToken verifies the Pub/Sub push token (if configured)
|
||||
func (h *SubscriptionWebhookHandler) VerifyGooglePubSubToken(c *gin.Context) bool {
|
||||
func (h *SubscriptionWebhookHandler) VerifyGooglePubSubToken(c echo.Context) bool {
|
||||
// If you configured a push endpoint with authentication, verify here
|
||||
// The token is typically in the Authorization header
|
||||
|
||||
|
||||
Reference in New Issue
Block a user