fix(webhooks): replace parity stubs

This commit is contained in:
2026-06-07 13:18:25 +08:00
parent 7ea4bf9362
commit c28eb010c6
4 changed files with 75 additions and 51 deletions
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -1,6 +1,6 @@
# Phase 6 Placeholder Audit
Updated: 2026-06-06T17:53:18Z
Updated: 2026-06-07
## Commands
@@ -13,11 +13,11 @@ scripts/parity_frontend_smoke.sh --check
## Result
`chatwootParityStub` appears only in `internal/router/router.go`: 20 call sites plus the helper definition.
`chatwootParityStub` has been removed from product code. The exact scan returns no matches.
The broader placeholder scan still has no reused-frontend critical account/contact/conversation/message/inbox/widget/public handler placeholder. Non-stub matches are test doubles, expected validation errors such as unsupported file/search/bulk-action operations, excluded SSO/SAML placeholder notes, or provider-specific nil-handler fallbacks.
The broader placeholder scan still has no reused-frontend critical account/contact/conversation/message/inbox/widget/public handler placeholder. Non-stub matches are test doubles, expected validation errors such as unsupported file/search/bulk-action operations, excluded SSO/SAML placeholder notes, or explicit assertions that nil webhook fallbacks do not return placeholder bodies.
All call sites are nil-handler guards for public webhook registration paths:
The previously remaining call sites were nil-handler guards for public webhook registration paths:
- Telegram webhook callback.
- WhatsApp verify and event callbacks.
@@ -28,11 +28,11 @@ All call sites are nil-handler guards for public webhook registration paths:
- Instagram verify and event callbacks.
- Shopify webhook callback.
No `chatwootParityStub` call site is registered on the reused dashboard account/contact/conversation/message/inbox API path, widget API path, public help-center path, public inbox/contact/conversation/message API path, or enterprise settings API path.
No `chatwootParityStub` call site remains on any reused dashboard account/contact/conversation/message/inbox API path, widget API path, public help-center path, public inbox/contact/conversation/message API path, enterprise settings API path, or webhook fallback path.
## Classification
The remaining stubs are defensive nil-handler fallbacks. In normal app bootstrap the matching provider handlers are wired and these branches do not define the route behavior. Provider-specific webhook behavior remains owned by the existing webhook/channel parity slices and future B12 or provider fixture failures.
The former defensive nil-handler fallbacks now return explicit `503 { error: "webhook provider unavailable", message: "webhook handler is not configured" }` responses instead of placeholder/not-implemented bodies. In normal app bootstrap the matching provider handlers are wired and these branches do not define the route behavior. Provider-specific webhook behavior remains owned by the existing webhook/channel parity slices and future B12 or provider fixture failures.
Phase 6 is therefore in Review for the inspected placeholder surface. Reopen it only if a fresh `rg` audit or reused frontend smoke identifies a frontend-reachable `chatwootParityStub`, `not implemented`, or placeholder success response in account/contact/conversation/message/inbox/widget/public critical paths.
+24 -31
View File
@@ -358,7 +358,7 @@ func RegisterRoutes(
tgGroup := webhookGroup.Group("/telegram")
tgGroup.POST("/:bot_token", func(c *gin.Context) {
if handlers == nil || handlers.TelegramWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TelegramWebhook.HandleTelegramWebhook(c)
@@ -372,14 +372,14 @@ func RegisterRoutes(
waGroup := webhookGroup.Group("/whatsapp")
waGroup.GET("/:phone_number", func(c *gin.Context) {
if handlers == nil || handlers.WhatsAppWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.WhatsAppWebhook.HandleWhatsAppVerification(c)
})
waGroup.POST("/:phone_number", func(c *gin.Context) {
if handlers == nil || handlers.WhatsAppWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.WhatsAppWebhook.HandleWhatsAppWebhook(c)
@@ -392,21 +392,21 @@ func RegisterRoutes(
ttGroup := webhookGroup.Group("/tiktok")
ttGroup.POST("", func(c *gin.Context) {
if handlers == nil || handlers.TikTokWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TikTokWebhook.HandleTikTokWebhook(c)
})
ttGroup.GET("/:business_id", func(c *gin.Context) {
if handlers == nil || handlers.TikTokWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TikTokWebhook.HandleTikTokVerification(c)
})
ttGroup.POST("/:business_id", func(c *gin.Context) {
if handlers == nil || handlers.TikTokWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TikTokWebhook.HandleTikTokWebhook(c)
@@ -418,7 +418,7 @@ func RegisterRoutes(
lineGroup := webhookGroup.Group("/line")
lineGroup.POST("/:line_channel_id", func(c *gin.Context) {
if handlers == nil || handlers.LineWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.LineWebhook.HandleLineWebhook(c)
@@ -430,7 +430,7 @@ func RegisterRoutes(
// Reference: Twilio SMS API https://www.twilio.com/docs/sms/api/message-resource
webhookGroup.POST("/sms/:phone_number", func(c *gin.Context) {
if handlers == nil || handlers.TwilioWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwilioWebhook.HandleTwilioInboundSMS(c)
@@ -438,35 +438,35 @@ func RegisterRoutes(
twilioGroup := webhookGroup.Group("/twilio")
twilioGroup.POST("/sms/:phone_number", func(c *gin.Context) {
if handlers == nil || handlers.TwilioWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwilioWebhook.HandleTwilioInboundSMS(c)
})
twilioGroup.POST("/status/:phone_number", func(c *gin.Context) {
if handlers == nil || handlers.TwilioWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c)
})
twilioGroup.POST("/delivery_status", func(c *gin.Context) {
if handlers == nil || handlers.TwilioWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c)
})
engine.POST("/twilio/delivery_status", func(c *gin.Context) {
if handlers == nil || handlers.TwilioWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwilioWebhook.HandleTwilioDeliveryStatus(c)
})
engine.POST("/twilio/callback", func(c *gin.Context) {
if handlers == nil || handlers.TwilioWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwilioWebhook.HandleTwilioCallback(c)
@@ -485,14 +485,14 @@ func RegisterRoutes(
twWebhookGroup := webhookGroup.Group("/twitter")
twWebhookGroup.GET("", func(c *gin.Context) {
if handlers == nil || handlers.TwitterChannel == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwitterChannel.WebhookCRC(c)
})
twWebhookGroup.POST("", func(c *gin.Context) {
if handlers == nil || handlers.TwitterChannel == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwitterChannel.WebhookEvent(c)
@@ -500,14 +500,14 @@ func RegisterRoutes(
// Legacy GoChat aliases kept for already configured Twitter webhooks.
twWebhookGroup.GET("/webhook", func(c *gin.Context) {
if handlers == nil || handlers.TwitterChannel == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwitterChannel.WebhookCRC(c)
})
twWebhookGroup.POST("/webhook", func(c *gin.Context) {
if handlers == nil || handlers.TwitterChannel == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.TwitterChannel.WebhookEvent(c)
@@ -515,21 +515,21 @@ func RegisterRoutes(
webhookGroup.GET("/instagram", func(c *gin.Context) {
if handlers == nil || handlers.FacebookWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.FacebookWebhook.HandleInstagramVerification(c)
})
webhookGroup.POST("/instagram", func(c *gin.Context) {
if handlers == nil || handlers.FacebookWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.FacebookWebhook.HandleInstagramWebhook(c)
})
webhookGroup.POST("/shopify", func(c *gin.Context) {
if handlers == nil || handlers.ShopifyWebhook == nil {
chatwootParityStub(c)
webhookProviderUnavailable(c)
return
}
handlers.ShopifyWebhook.HandleShopifyWebhook(c)
@@ -2056,20 +2056,13 @@ func registerWidgetRoutes(g *gin.RouterGroup, h *widget.WidgetHandler) {
widget.POST("/:website_token/offline_message", h.SubmitOfflineMessage)
}
func chatwootParityStub(c *gin.Context) {
c.JSON(501, gin.H{
"error": "not implemented",
"message": "Chatwoot parity route is registered; behavior is tracked for implementation",
func webhookProviderUnavailable(c *gin.Context) {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "webhook provider unavailable",
"message": "webhook handler is not configured",
})
}
// --- Placeholder handlers for webhooks ---
// Full implementation deferred to P7 (channels)
func webhookStub(c *gin.Context) {
c.JSON(200, gin.H{"message": "Webhook callback placeholder (P7)"})
}
func healthCheck(c *gin.Context) {
uptime := time.Since(startTime)
c.JSON(200, gin.H{
+29
View File
@@ -98,6 +98,35 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
}
}
func TestWebhookNilHandlerReturnsProviderUnavailable(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
RegisterRoutes(
engine,
nil,
nil,
nil,
&Handlers{},
nil,
nil,
&config.JWTConfig{},
middleware.CORSConfig{},
nil,
)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/webhooks/telegram/bot-token", nil)
engine.ServeHTTP(w, req)
if w.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, w.Code)
}
if strings.Contains(w.Body.String(), "not implemented") || strings.Contains(w.Body.String(), "placeholder") {
t.Fatalf("nil webhook fallback returned placeholder body: %s", w.Body.String())
}
}
func TestTwilioVoiceRoutesServeConferenceAndPersistCallbacks(t *testing.T) {
gin.SetMode(gin.TestMode)
db, call := setupRouterTwilioVoiceDB(t)