feat(integrations): align app hook payloads
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/pkg/pagination"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
@@ -37,24 +39,19 @@ func (h *IntegrationHookHandler) ListApps(c *gin.Context) {
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, apps)
|
||||
accountID := getAccountID(c)
|
||||
c.JSON(http.StatusOK, gin.H{"payload": h.serializeIntegrationApps(c, accountID, apps)})
|
||||
}
|
||||
|
||||
// GetApp retrieves a single integration app by ID.
|
||||
// GET /api/v1/accounts/:account_id/integrations/apps/:id
|
||||
func (h *IntegrationHookHandler) GetApp(c *gin.Context) {
|
||||
id, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
app, svcErr := h.svc.GetApp(c.Request.Context(), id)
|
||||
app, svcErr := h.svc.GetAppByID(c.Request.Context(), c.Param("id"))
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, app)
|
||||
c.JSON(http.StatusOK, h.serializeIntegrationApp(c, getAccountID(c), *app))
|
||||
}
|
||||
|
||||
// --- Integration Hooks CRUD ---
|
||||
@@ -86,12 +83,12 @@ func (h *IntegrationHookHandler) GetHook(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
hook, svcErr := h.svc.Get(c.Request.Context(), id)
|
||||
hook, svcErr := h.svc.GetScoped(c.Request.Context(), getAccountID(c), id)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, hook)
|
||||
c.JSON(http.StatusOK, serializeIntegrationHook(*hook))
|
||||
}
|
||||
|
||||
// CreateHook creates a new integration hook.
|
||||
@@ -103,22 +100,18 @@ func (h *IntegrationHookHandler) CreateHook(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Chatwoot: params.require(:hook) → {"hook": {...}}
|
||||
var wrapper struct {
|
||||
Hook service.CreateHookRequest `json:"hook"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
var req service.CreateHookRequest
|
||||
if err := bindJSONWrappedOrRaw(c, "hook", &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req := wrapper.Hook
|
||||
|
||||
hook, svcErr := h.svc.Create(c.Request.Context(), accountID, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, hook)
|
||||
c.JSON(http.StatusOK, serializeIntegrationHook(*hook))
|
||||
}
|
||||
|
||||
// UpdateHook updates an existing integration hook.
|
||||
@@ -130,22 +123,19 @@ func (h *IntegrationHookHandler) UpdateHook(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Chatwoot: params.require(:hook) → {"hook": {...}}
|
||||
var wrapper struct {
|
||||
Hook service.UpdateHookRequest `json:"hook"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
accountID := getAccountID(c)
|
||||
var req service.UpdateHookRequest
|
||||
if err := bindJSONWrappedOrRaw(c, "hook", &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req := wrapper.Hook
|
||||
|
||||
hook, svcErr := h.svc.Update(c.Request.Context(), id, req)
|
||||
hook, svcErr := h.svc.UpdateScoped(c.Request.Context(), accountID, id, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, hook)
|
||||
c.JSON(http.StatusOK, serializeIntegrationHook(*hook))
|
||||
}
|
||||
|
||||
// DeleteHook deletes an integration hook.
|
||||
@@ -157,11 +147,11 @@ func (h *IntegrationHookHandler) DeleteHook(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.Delete(c.Request.Context(), id); svcErr != nil {
|
||||
if svcErr := h.svc.DeleteScoped(c.Request.Context(), getAccountID(c), id); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"id": id})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// ProcessHookEvent processes an incoming event for a hook.
|
||||
@@ -187,7 +177,143 @@ func (h *IntegrationHookHandler) ProcessHookEvent(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, svcErr.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"message": "event processed"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "event processed"})
|
||||
}
|
||||
|
||||
func (h *IntegrationHookHandler) serializeIntegrationApps(c *gin.Context, accountID uint, apps []model.IntegrationApp) []gin.H {
|
||||
payload := make([]gin.H, 0, len(apps))
|
||||
for _, app := range apps {
|
||||
payload = append(payload, h.serializeIntegrationApp(c, accountID, app))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func (h *IntegrationHookHandler) serializeIntegrationApp(c *gin.Context, accountID uint, app model.IntegrationApp) gin.H {
|
||||
appID := integrationAppID(app)
|
||||
hooks, _ := h.svc.ListHooksForApp(c.Request.Context(), accountID, appID)
|
||||
serializedHooks := make([]gin.H, 0, len(hooks))
|
||||
for _, hook := range hooks {
|
||||
serializedHooks = append(serializedHooks, serializeIntegrationHook(hook))
|
||||
}
|
||||
return gin.H{
|
||||
"id": appID,
|
||||
"name": app.Name,
|
||||
"description": app.Description,
|
||||
"short_description": app.Description,
|
||||
"enabled": len(serializedHooks) > 0,
|
||||
"action": app.ActionURL,
|
||||
"button": app.ActionURL,
|
||||
"hook_type": integrationAppHookType(appID),
|
||||
"allow_multiple_hooks": integrationAppAllowsMultipleHooks(appID),
|
||||
"settings_form_schema": integrationAppSettingsFormSchema(appID),
|
||||
"visible_properties": integrationAppVisibleProperties(appID),
|
||||
"hooks": serializedHooks,
|
||||
}
|
||||
}
|
||||
|
||||
func serializeIntegrationHook(hook model.IntegrationHook) gin.H {
|
||||
settings := gin.H{}
|
||||
if len(hook.Settings) > 0 {
|
||||
_ = json.Unmarshal(hook.Settings, &settings)
|
||||
}
|
||||
payload := gin.H{
|
||||
"id": hook.ID,
|
||||
"app_id": integrationHookAppID(hook),
|
||||
"status": hook.Status != model.HookStatusInactive,
|
||||
"account_id": hook.AccountID,
|
||||
"hook_type": integrationHookKind(hook),
|
||||
"settings": settings,
|
||||
}
|
||||
if hook.ReferenceID != "" {
|
||||
payload["reference_id"] = hook.ReferenceID
|
||||
}
|
||||
if hook.InboxID != nil && *hook.InboxID != 0 {
|
||||
inbox := gin.H{"id": *hook.InboxID}
|
||||
if hook.Inbox != nil && hook.Inbox.ID != 0 {
|
||||
inbox["name"] = hook.Inbox.Name
|
||||
}
|
||||
payload["inbox"] = inbox
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func integrationAppID(app model.IntegrationApp) string {
|
||||
if app.HookType != "" {
|
||||
return string(app.HookType)
|
||||
}
|
||||
return strings.ToLower(strings.ReplaceAll(app.Name, " ", "_"))
|
||||
}
|
||||
|
||||
func integrationHookAppID(hook model.IntegrationHook) string {
|
||||
if hook.AppID != "" {
|
||||
return hook.AppID
|
||||
}
|
||||
return string(hook.HookType)
|
||||
}
|
||||
|
||||
func integrationAppHookType(appID string) string {
|
||||
if appID == "dialogflow" {
|
||||
return "inbox"
|
||||
}
|
||||
return "account"
|
||||
}
|
||||
|
||||
func integrationAppAllowsMultipleHooks(appID string) bool {
|
||||
return appID == "webhook" || appID == "dashboard_apps" || appID == "dialogflow"
|
||||
}
|
||||
|
||||
func integrationAppSettingsFormSchema(appID string) []gin.H {
|
||||
schemas := map[string][]gin.H{
|
||||
"openai": {
|
||||
{"label": "API Key", "type": "text", "name": "api_key", "validation": "required"},
|
||||
{"label": "Show label suggestions", "type": "checkbox", "name": "label_suggestion", "validation": ""},
|
||||
},
|
||||
"dialogflow": {
|
||||
{"label": "Dialogflow Project ID", "type": "text", "name": "project_id", "validation": "required", "validationName": "Project Id"},
|
||||
{"label": "Dialogflow Project Key File", "type": "textarea", "name": "credentials", "validation": "required|JSON", "validationName": "Credentials", "validation-messages": gin.H{"JSON": "Invalid JSON", "required": "Credentials is required"}},
|
||||
{"label": "Dialogflow Region", "type": "select", "name": "region", "default": "global", "options": []gin.H{{"label": "Global - Default", "value": "global"}, {"label": "AS-NE1 - Tokyo, Japan", "value": "asia-northeast1"}, {"label": "AU-SE1 - Sydney, Australia", "value": "australia-southeast1"}, {"label": "EU-W1 - St. Ghislain, Belgium", "value": "europe-west1"}, {"label": "EU-W2 - London, England", "value": "europe-west2"}}},
|
||||
{"label": "Language Code", "type": "select", "name": "language_code", "default": "en-US", "help": "Language code for Dialogflow agent. Use \"auto\" to detect from contact language.", "options": []gin.H{{"label": "Auto-detect from contact", "value": "auto"}, {"label": "English (US)", "value": "en-US"}, {"label": "English (UK)", "value": "en-GB"}, {"label": "Spanish (Spain)", "value": "es-ES"}, {"label": "Spanish (Latin America)", "value": "es-419"}, {"label": "French", "value": "fr-FR"}, {"label": "German", "value": "de-DE"}, {"label": "Portuguese (Brazil)", "value": "pt-BR"}, {"label": "Portuguese (Portugal)", "value": "pt-PT"}, {"label": "Italian", "value": "it-IT"}, {"label": "Japanese", "value": "ja-JP"}, {"label": "Korean", "value": "ko-KR"}, {"label": "Chinese (Simplified)", "value": "zh-CN"}, {"label": "Chinese (Traditional)", "value": "zh-TW"}, {"label": "Hindi", "value": "hi-IN"}, {"label": "Arabic", "value": "ar"}, {"label": "Russian", "value": "ru-RU"}, {"label": "Dutch", "value": "nl-NL"}, {"label": "Polish", "value": "pl-PL"}, {"label": "Turkish", "value": "tr-TR"}, {"label": "Thai", "value": "th-TH"}, {"label": "Vietnamese", "value": "vi-VN"}, {"label": "Indonesian", "value": "id-ID"}}},
|
||||
},
|
||||
"google_translate": {
|
||||
{"label": "Google Cloud Project ID", "type": "text", "name": "project_id", "validation": "required", "validationName": "Project Id"},
|
||||
{"label": "Google Cloud Project Key File", "type": "textarea", "name": "credentials", "validation": "required|JSON", "validationName": "Credentials", "validation-messages": gin.H{"JSON": "Invalid JSON", "required": "Credentials is required"}},
|
||||
},
|
||||
"dyte": {
|
||||
{"label": "Organization ID", "type": "text", "name": "organization_id", "validation": "required"},
|
||||
{"label": "API Key", "type": "text", "name": "api_key", "validation": "required"},
|
||||
},
|
||||
"leadsquared": {
|
||||
{"label": "Access Key", "type": "text", "name": "access_key", "validation": "required"},
|
||||
{"label": "Secret Key", "type": "text", "name": "secret_key", "validation": "required"},
|
||||
{"label": "Endpoint URL", "type": "text", "name": "endpoint_url", "validation": "required|url"},
|
||||
{"label": "App URL", "type": "text", "name": "app_url", "validation": "required|url"},
|
||||
},
|
||||
}
|
||||
if schema, ok := schemas[appID]; ok {
|
||||
return schema
|
||||
}
|
||||
return []gin.H{}
|
||||
}
|
||||
|
||||
func integrationAppVisibleProperties(appID string) []string {
|
||||
properties := map[string][]string{
|
||||
"openai": {"api_key", "label_suggestion"},
|
||||
"dialogflow": {"project_id", "region", "language_code"},
|
||||
"google_translate": {"project_id"},
|
||||
"dyte": {"organization_id"},
|
||||
"leadsquared": {"access_key", "endpoint_url", "app_url"},
|
||||
}
|
||||
if visibleProperties, ok := properties[appID]; ok {
|
||||
return visibleProperties
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func integrationHookKind(hook model.IntegrationHook) string {
|
||||
if hook.InboxID != nil && *hook.InboxID != 0 {
|
||||
return "inbox"
|
||||
}
|
||||
return "account"
|
||||
}
|
||||
|
||||
// RegisterIntegrationHookRoutes registers integration hook routes on a router group.
|
||||
@@ -195,6 +321,7 @@ func RegisterIntegrationHookRoutes(g *gin.RouterGroup, h *IntegrationHookHandler
|
||||
// Integration apps catalog
|
||||
apps := g.Group("/apps")
|
||||
{
|
||||
apps.GET("", h.ListApps)
|
||||
apps.GET("/", h.ListApps)
|
||||
apps.GET("/:id", h.GetApp)
|
||||
}
|
||||
@@ -202,10 +329,13 @@ func RegisterIntegrationHookRoutes(g *gin.RouterGroup, h *IntegrationHookHandler
|
||||
// Integration hooks CRUD + process event
|
||||
hooks := g.Group("/hooks")
|
||||
{
|
||||
hooks.GET("", h.ListHooks)
|
||||
hooks.GET("/", h.ListHooks)
|
||||
hooks.GET("/:id", h.GetHook)
|
||||
hooks.POST("", h.CreateHook)
|
||||
hooks.POST("/", h.CreateHook)
|
||||
hooks.PUT("/:id", h.UpdateHook)
|
||||
hooks.PATCH("/:id", h.UpdateHook)
|
||||
hooks.DELETE("/:id", h.DeleteHook)
|
||||
hooks.POST("/:id/process_event", h.ProcessHookEvent)
|
||||
}
|
||||
|
||||
@@ -53,20 +53,7 @@ func (s *IntegrationHookHandlerSuite) SetupSuite() {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery(), mockAuthMiddlewareForHook())
|
||||
g := r.Group("/api/v1/accounts/:account_id/integrations")
|
||||
apps := g.Group("/apps")
|
||||
{
|
||||
apps.GET("/", s.handler.ListApps)
|
||||
apps.GET("/:id", s.handler.GetApp)
|
||||
}
|
||||
hooks := g.Group("/hooks")
|
||||
{
|
||||
hooks.GET("/", s.handler.ListHooks)
|
||||
hooks.GET("/:id", s.handler.GetHook)
|
||||
hooks.POST("/", s.handler.CreateHook)
|
||||
hooks.PUT("/:id", s.handler.UpdateHook)
|
||||
hooks.DELETE("/:id", s.handler.DeleteHook)
|
||||
hooks.POST("/:id/process_event", s.handler.ProcessHookEvent)
|
||||
}
|
||||
RegisterIntegrationHookRoutes(g, s.handler)
|
||||
s.router = r
|
||||
}
|
||||
|
||||
@@ -105,6 +92,7 @@ func (s *IntegrationHookHandlerSuite) createHook(accountID uint, hookType model.
|
||||
token := "test_token_" + strconv.Itoa(hookTokenCounter)
|
||||
hook := &model.IntegrationHook{
|
||||
AccountID: accountID,
|
||||
AppID: string(hookType),
|
||||
HookType: hookType,
|
||||
Status: model.HookStatusActive,
|
||||
URL: url,
|
||||
@@ -129,10 +117,10 @@ func (s *IntegrationHookHandlerSuite) TestListApps_Success() {
|
||||
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
s.Equal(true, body["success"])
|
||||
|
||||
data := body["data"].([]interface{})
|
||||
data := body["payload"].([]interface{})
|
||||
s.Len(data, 2)
|
||||
app := data[0].(map[string]interface{})
|
||||
s.Contains(app, "hooks")
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestListApps_Empty() {
|
||||
@@ -143,9 +131,7 @@ func (s *IntegrationHookHandlerSuite) TestListApps_Empty() {
|
||||
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
s.Equal(true, body["success"])
|
||||
|
||||
data := body["data"].([]interface{})
|
||||
data := body["payload"].([]interface{})
|
||||
s.Len(data, 0)
|
||||
}
|
||||
|
||||
@@ -154,26 +140,24 @@ func (s *IntegrationHookHandlerSuite) TestListApps_Empty() {
|
||||
// =====================
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestGetApp_Success() {
|
||||
app := s.createApp("Slack App", model.HookTypeSlack)
|
||||
s.createApp("Slack App", model.HookTypeSlack)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/"+s.uid(app.ID), nil)
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/slack", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
s.Equal(true, body["success"])
|
||||
|
||||
data := body["data"].(map[string]interface{})
|
||||
s.Equal("Slack App", data["name"])
|
||||
s.Equal("slack", body["id"])
|
||||
s.Equal("Slack App", body["name"])
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestGetApp_InvalidID() {
|
||||
func (s *IntegrationHookHandlerSuite) TestGetApp_UnknownID() {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/abc", nil)
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/unknown", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
s.Equal(http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestGetApp_NotFound() {
|
||||
@@ -232,11 +216,9 @@ func (s *IntegrationHookHandlerSuite) TestGetHook_Success() {
|
||||
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
s.Equal(true, body["success"])
|
||||
|
||||
data := body["data"].(map[string]interface{})
|
||||
s.Equal("webhook", data["hook_type"])
|
||||
s.Equal("https://example.com/hook", data["url"])
|
||||
s.Equal("webhook", body["app_id"])
|
||||
s.Equal("account", body["hook_type"])
|
||||
s.Equal(true, body["status"])
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestGetHook_NotFound() {
|
||||
@@ -251,10 +233,12 @@ func (s *IntegrationHookHandlerSuite) TestGetHook_NotFound() {
|
||||
// =====================
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestCreateHook_Success() {
|
||||
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]string{
|
||||
"hook_type": "webhook",
|
||||
"url": "https://example.com/new_hook",
|
||||
}})
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"app_id": "webhook",
|
||||
"settings": map[string]interface{}{
|
||||
"project_id": "project-1",
|
||||
},
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/", bytes.NewBuffer(body))
|
||||
@@ -264,12 +248,12 @@ func (s *IntegrationHookHandlerSuite) TestCreateHook_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(true, resp["success"])
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
s.Equal("webhook", data["hook_type"])
|
||||
s.Equal("https://example.com/new_hook", data["url"])
|
||||
s.NotNil(data["id"])
|
||||
s.Equal("webhook", resp["app_id"])
|
||||
s.Equal("account", resp["hook_type"])
|
||||
s.Equal(true, resp["status"])
|
||||
s.NotNil(resp["id"])
|
||||
settings := resp["settings"].(map[string]interface{})
|
||||
s.Equal("project-1", settings["project_id"])
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestCreateHook_InvalidJSON() {
|
||||
@@ -295,9 +279,12 @@ func (s *IntegrationHookHandlerSuite) TestCreateHook_InvalidAccountID() {
|
||||
func (s *IntegrationHookHandlerSuite) TestUpdateHook_Success() {
|
||||
hook := s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
|
||||
|
||||
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]string{
|
||||
"url": "https://example.com/updated_hook",
|
||||
"status": "inactive",
|
||||
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]interface{}{
|
||||
"status": "disabled",
|
||||
"reference_id": "ref-123",
|
||||
"settings": map[string]interface{}{
|
||||
"channel": "support",
|
||||
},
|
||||
}})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -308,11 +295,10 @@ func (s *IntegrationHookHandlerSuite) TestUpdateHook_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(true, resp["success"])
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
s.Equal("https://example.com/updated_hook", data["url"])
|
||||
s.Equal("inactive", data["status"])
|
||||
s.Equal(false, resp["status"])
|
||||
s.Equal("ref-123", resp["reference_id"])
|
||||
settings := resp["settings"].(map[string]interface{})
|
||||
s.Equal("support", settings["channel"])
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestUpdateHook_NotFound() {
|
||||
@@ -357,21 +343,14 @@ func (s *IntegrationHookHandlerSuite) TestDeleteHook_Success() {
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(true, resp["success"])
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
s.Equal(float64(hook.ID), data["id"])
|
||||
s.Empty(w.Body.String())
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestDeleteHook_NotFound() {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/integrations/hooks/9999", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
// GORM soft-delete on non-existent ID returns nil error → handler returns 200 with {"id": 9999}
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.Equal(http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestDeleteHook_InvalidID() {
|
||||
@@ -401,10 +380,7 @@ func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(true, resp["success"])
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
s.Equal("event processed", data["message"])
|
||||
s.Equal("event processed", resp["message"])
|
||||
}
|
||||
|
||||
func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_NotFound() {
|
||||
@@ -442,6 +418,7 @@ func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_InactiveHook() {
|
||||
hookTokenCounter++
|
||||
hook := &model.IntegrationHook{
|
||||
AccountID: 1,
|
||||
AppID: "webhook",
|
||||
HookType: model.HookTypeWebhook,
|
||||
Status: model.HookStatusInactive,
|
||||
URL: "https://example.com/hook",
|
||||
@@ -462,4 +439,4 @@ func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_InactiveHook() {
|
||||
// TestIntegrationHookHandlerSuite runs the suite.
|
||||
func TestIntegrationHookHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationHookHandlerSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,15 @@ import (
|
||||
type IntegrationHook struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
AccountID uint `gorm:"not null;index" json:"account_id"`
|
||||
InboxID *uint `gorm:"index" json:"inbox_id,omitempty"` // nil = account-level hook
|
||||
HookType HookType `gorm:"size:50;not null;index" json:"hook_type"` // webhook/slack/shopify/linear/notion
|
||||
Status HookStatus `gorm:"size:20;default:'active'" json:"status"` // active/inactive
|
||||
URL string `gorm:"size:1024" json:"url"` // webhook callback URL
|
||||
AppID string `gorm:"size:100;index" json:"app_id,omitempty"`
|
||||
InboxID *uint `gorm:"index" json:"inbox_id,omitempty"` // nil = account-level hook
|
||||
Inbox *Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
|
||||
HookType HookType `gorm:"size:50;not null;index" json:"hook_type"` // webhook/slack/shopify/linear/notion
|
||||
Status HookStatus `gorm:"size:20;default:'active'" json:"status"` // active/inactive
|
||||
URL string `gorm:"size:1024" json:"url"` // webhook callback URL
|
||||
AccessToken string `gorm:"size:256;uniqueIndex" json:"access_token,omitempty"` // API token for the hook
|
||||
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // provider-specific config (Slack channel_id, Shopify shop_domain, etc.)
|
||||
ReferenceID string `gorm:"size:255" json:"reference_id,omitempty"`
|
||||
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // provider-specific config (Slack channel_id, Shopify shop_domain, etc.)
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
|
||||
@@ -38,15 +41,15 @@ type IntegrationHook struct {
|
||||
// IntegrationApp represents an available integration app definition.
|
||||
// Reference: Chatwoot Integrations::App — catalog of supported integrations
|
||||
type IntegrationApp struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
HookType HookType `gorm:"size:50;not null;uniqueIndex" json:"hook_type"`
|
||||
Icon string `gorm:"size:512" json:"icon"` // icon URL or CSS class
|
||||
ActionURL string `gorm:"size:1024" json:"action_url"` // frontend action URL for the integration setup page
|
||||
Enabled bool `gorm:"default:true" json:"enabled"` // whether the integration is available for installation
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Description string `gorm:"size:512" json:"description"`
|
||||
HookType HookType `gorm:"size:50;not null;uniqueIndex" json:"hook_type"`
|
||||
Icon string `gorm:"size:512" json:"icon"` // icon URL or CSS class
|
||||
ActionURL string `gorm:"size:1024" json:"action_url"` // frontend action URL for the integration setup page
|
||||
Enabled bool `gorm:"default:true" json:"enabled"` // whether the integration is available for installation
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
// SlackSettings holds Slack integration-specific configuration.
|
||||
@@ -60,7 +63,7 @@ type SlackSettings struct {
|
||||
// ShopifySettings holds Shopify integration-specific configuration.
|
||||
// Reference: Chatwoot Integrations::ShopifyController settings
|
||||
type ShopifySettings struct {
|
||||
ShopDomain string `json:"shop_domain"`
|
||||
ShopDomain string `json:"shop_domain"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,19 @@ func NewIntegrationHookRepo(db *gorm.DB) *IntegrationHookRepo {
|
||||
// GetByID retrieves an integration hook by primary key.
|
||||
func (r *IntegrationHookRepo) GetByID(ctx context.Context, id uint) (*model.IntegrationHook, error) {
|
||||
var hook model.IntegrationHook
|
||||
if err := r.db.WithContext(ctx).First(&hook, id).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Preload("Inbox").First(&hook, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hook, nil
|
||||
}
|
||||
|
||||
// GetByAccountAndID retrieves an integration hook scoped to an account.
|
||||
func (r *IntegrationHookRepo) GetByAccountAndID(ctx context.Context, accountID, id uint) (*model.IntegrationHook, error) {
|
||||
var hook model.IntegrationHook
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("Inbox").
|
||||
Where("account_id = ?", accountID).
|
||||
First(&hook, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hook, nil
|
||||
@@ -38,10 +50,22 @@ func (r *IntegrationHookRepo) FindByAccount(ctx context.Context, accountID uint,
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := q.Offset(offset).Limit(limit).Order("id ASC").Find(&hooks).Error
|
||||
err := q.Preload("Inbox").Offset(offset).Limit(limit).Order("id ASC").Find(&hooks).Error
|
||||
return hooks, total, err
|
||||
}
|
||||
|
||||
// FindByAccountAndApp retrieves integration hooks for a Chatwoot app id.
|
||||
func (r *IntegrationHookRepo) FindByAccountAndApp(ctx context.Context, accountID uint, appID string) ([]model.IntegrationHook, error) {
|
||||
var hooks []model.IntegrationHook
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Inbox").
|
||||
Where("account_id = ?", accountID).
|
||||
Where("app_id = ? OR (app_id = '' AND hook_type = ?)", appID, appID).
|
||||
Order("id ASC").
|
||||
Find(&hooks).Error
|
||||
return hooks, err
|
||||
}
|
||||
|
||||
// FindByAccountAndType retrieves integration hooks for an account filtered by hook type.
|
||||
func (r *IntegrationHookRepo) FindByAccountAndType(ctx context.Context, accountID uint, hookType model.HookType) ([]model.IntegrationHook, error) {
|
||||
var hooks []model.IntegrationHook
|
||||
@@ -114,3 +138,8 @@ func (r *IntegrationAppRepo) GetByHookType(ctx context.Context, hookType model.H
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// GetByAppID retrieves an integration app by Chatwoot app id.
|
||||
func (r *IntegrationAppRepo) GetByAppID(ctx context.Context, appID string) (*model.IntegrationApp, error) {
|
||||
return r.GetByHookType(ctx, model.HookType(appID))
|
||||
}
|
||||
|
||||
@@ -39,9 +39,10 @@ func (s *IntegrationHookService) SetRegistry(registry *WebhookProcessorRegistry)
|
||||
}
|
||||
|
||||
// CreateHookRequest is the DTO for creating an integration hook.
|
||||
// Reference: Chatwoot HooksController#create — params: {hook_type, url, inbox_id, settings}
|
||||
// Reference: Chatwoot HooksController#create — params: {app_id, inbox_id, settings}
|
||||
type CreateHookRequest struct {
|
||||
HookType string `json:"hook_type" validate:"required,oneof=webhook slack shopify linear notion"`
|
||||
AppID string `json:"app_id,omitempty"`
|
||||
HookType string `json:"hook_type,omitempty"`
|
||||
URL string `json:"url,omitempty" validate:"omitempty,url"`
|
||||
InboxID *uint `json:"inbox_id,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
@@ -49,9 +50,24 @@ type CreateHookRequest struct {
|
||||
|
||||
// UpdateHookRequest is the DTO for updating an integration hook.
|
||||
type UpdateHookRequest struct {
|
||||
URL string `json:"url,omitempty" validate:"omitempty,url"`
|
||||
Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
URL string `json:"url,omitempty" validate:"omitempty,url"`
|
||||
Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive enabled disabled"`
|
||||
ReferenceID string `json:"reference_id,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
var supportedIntegrationAppIDs = map[string]struct{}{
|
||||
"webhook": {},
|
||||
"dashboard_apps": {},
|
||||
"slack": {},
|
||||
"shopify": {},
|
||||
"linear": {},
|
||||
"notion": {},
|
||||
"dialogflow": {},
|
||||
"openai": {},
|
||||
"google_translate": {},
|
||||
"dyte": {},
|
||||
"leadsquared": {},
|
||||
}
|
||||
|
||||
// List returns integration hooks for an account, paginated.
|
||||
@@ -64,11 +80,20 @@ func (s *IntegrationHookService) Get(ctx context.Context, id uint) (*model.Integ
|
||||
return s.hookRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetScoped returns a single integration hook scoped to an account.
|
||||
func (s *IntegrationHookService) GetScoped(ctx context.Context, accountID, id uint) (*model.IntegrationHook, error) {
|
||||
return s.hookRepo.GetByAccountAndID(ctx, accountID, id)
|
||||
}
|
||||
|
||||
// Create creates a new integration hook for an account.
|
||||
func (s *IntegrationHookService) Create(ctx context.Context, accountID uint, req CreateHookRequest) (*model.IntegrationHook, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
appID := normalizeIntegrationAppID(req)
|
||||
if err := validateIntegrationAppID(appID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate a unique access token for the hook
|
||||
token, err := generateHookAccessToken()
|
||||
@@ -78,8 +103,9 @@ func (s *IntegrationHookService) Create(ctx context.Context, accountID uint, req
|
||||
|
||||
hook := &model.IntegrationHook{
|
||||
AccountID: accountID,
|
||||
AppID: appID,
|
||||
InboxID: req.InboxID,
|
||||
HookType: model.HookType(req.HookType),
|
||||
HookType: model.HookType(appID),
|
||||
Status: model.HookStatusActive,
|
||||
URL: req.URL,
|
||||
AccessToken: token,
|
||||
@@ -131,6 +157,39 @@ func (s *IntegrationHookService) Update(ctx context.Context, id uint, req Update
|
||||
return hook, nil
|
||||
}
|
||||
|
||||
// UpdateScoped updates an existing integration hook scoped to an account.
|
||||
func (s *IntegrationHookService) UpdateScoped(ctx context.Context, accountID, id uint, req UpdateHookRequest) (*model.IntegrationHook, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
hook, err := s.hookRepo.GetByAccountAndID(ctx, accountID, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("integration hook not found: %w", err)
|
||||
}
|
||||
|
||||
if req.Status != "" {
|
||||
hook.Status = normalizeIntegrationHookStatus(req.Status)
|
||||
}
|
||||
if req.ReferenceID != "" {
|
||||
hook.ReferenceID = req.ReferenceID
|
||||
}
|
||||
if req.Settings != nil {
|
||||
settingsJSON, err := json.Marshal(req.Settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal settings: %w", err)
|
||||
}
|
||||
hook.Settings = settingsJSON
|
||||
}
|
||||
|
||||
if err := s.hookRepo.Update(ctx, hook); err != nil {
|
||||
return nil, fmt.Errorf("failed to update integration hook: %w", err)
|
||||
}
|
||||
|
||||
applogger.L().Infof("Integration hook updated: id=%d", id)
|
||||
return s.hookRepo.GetByAccountAndID(ctx, accountID, id)
|
||||
}
|
||||
|
||||
// Delete deletes an integration hook by ID.
|
||||
func (s *IntegrationHookService) Delete(ctx context.Context, id uint) error {
|
||||
if err := s.hookRepo.Delete(ctx, id); err != nil {
|
||||
@@ -140,6 +199,14 @@ func (s *IntegrationHookService) Delete(ctx context.Context, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteScoped deletes an integration hook scoped to an account.
|
||||
func (s *IntegrationHookService) DeleteScoped(ctx context.Context, accountID, id uint) error {
|
||||
if _, err := s.hookRepo.GetByAccountAndID(ctx, accountID, id); err != nil {
|
||||
return fmt.Errorf("integration hook not found: %w", err)
|
||||
}
|
||||
return s.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// ProcessEvent processes an incoming event for a hook (e.g., Slack slash command callback, Shopify webhook).
|
||||
// Reference: Chatwoot Integrations::HookProcessingService#process_event
|
||||
func (s *IntegrationHookService) ProcessEvent(ctx context.Context, hookID uint, eventData map[string]interface{}) error {
|
||||
@@ -164,11 +231,21 @@ func (s *IntegrationHookService) ListApps(ctx context.Context) ([]model.Integrat
|
||||
return s.appRepo.List(ctx)
|
||||
}
|
||||
|
||||
// ListHooksForApp returns account hooks for a Chatwoot integration app id.
|
||||
func (s *IntegrationHookService) ListHooksForApp(ctx context.Context, accountID uint, appID string) ([]model.IntegrationHook, error) {
|
||||
return s.hookRepo.FindByAccountAndApp(ctx, accountID, appID)
|
||||
}
|
||||
|
||||
// GetApp returns a single integration app by ID.
|
||||
func (s *IntegrationHookService) GetApp(ctx context.Context, id uint) (*model.IntegrationApp, error) {
|
||||
return s.appRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetAppByID returns a single integration app by Chatwoot app id.
|
||||
func (s *IntegrationHookService) GetAppByID(ctx context.Context, appID string) (*model.IntegrationApp, error) {
|
||||
return s.appRepo.GetByAppID(ctx, appID)
|
||||
}
|
||||
|
||||
// generateHookAccessToken creates a random 32-byte hex string for hook access tokens.
|
||||
func generateHookAccessToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
@@ -177,3 +254,31 @@ func generateHookAccessToken() (string, error) {
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func normalizeIntegrationAppID(req CreateHookRequest) string {
|
||||
if req.AppID != "" {
|
||||
return req.AppID
|
||||
}
|
||||
return req.HookType
|
||||
}
|
||||
|
||||
func validateIntegrationAppID(appID string) error {
|
||||
if appID == "" {
|
||||
return fmt.Errorf("app_id is required")
|
||||
}
|
||||
if _, ok := supportedIntegrationAppIDs[appID]; !ok {
|
||||
return fmt.Errorf("unsupported integration app: %s", appID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeIntegrationHookStatus(status string) model.HookStatus {
|
||||
switch status {
|
||||
case "enabled", "active":
|
||||
return model.HookStatusActive
|
||||
case "disabled", "inactive":
|
||||
return model.HookStatusInactive
|
||||
default:
|
||||
return model.HookStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ func TestIntegrationHookService_Create(t *testing.T) {
|
||||
}
|
||||
hook, err := svc.Create(ctx, 1, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "webhook", hook.AppID)
|
||||
assert.Equal(t, model.HookTypeWebhook, hook.HookType)
|
||||
assert.Equal(t, "https://example.com/webhook", hook.URL)
|
||||
assert.Equal(t, uint(1), hook.AccountID)
|
||||
@@ -71,9 +72,27 @@ func TestIntegrationHookService_Create_Slack(t *testing.T) {
|
||||
}
|
||||
hook, err := svc.Create(ctx, 1, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "slack", hook.AppID)
|
||||
assert.Equal(t, model.HookTypeSlack, hook.HookType)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_Create_WithAppID(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req := CreateHookRequest{
|
||||
AppID: "openai",
|
||||
Settings: map[string]interface{}{
|
||||
"api_key": "sk-test",
|
||||
},
|
||||
}
|
||||
hook, err := svc.Create(ctx, 1, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "openai", hook.AppID)
|
||||
assert.Equal(t, model.HookType("openai"), hook.HookType)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_Get(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil) // nil registry — webhook processing tests are separate
|
||||
@@ -91,6 +110,22 @@ func TestIntegrationHookService_Get(t *testing.T) {
|
||||
assert.Equal(t, hook.ID, found.ID)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_GetScoped(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
hook, err := svc.Create(ctx, 1, CreateHookRequest{AppID: "webhook"})
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := svc.GetScoped(ctx, 1, hook.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, hook.ID, found.ID)
|
||||
|
||||
_, err = svc.GetScoped(ctx, 2, hook.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_List(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil) // nil registry — webhook processing tests are separate
|
||||
@@ -133,6 +168,29 @@ func TestIntegrationHookService_Update(t *testing.T) {
|
||||
assert.Equal(t, model.HookStatusInactive, updated.Status)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_UpdateScoped(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
hook, err := svc.Create(ctx, 1, CreateHookRequest{AppID: "webhook"})
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := svc.UpdateScoped(ctx, 1, hook.ID, UpdateHookRequest{
|
||||
Status: "disabled",
|
||||
ReferenceID: "ref-1",
|
||||
Settings: map[string]interface{}{
|
||||
"channel": "support",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.HookStatusInactive, updated.Status)
|
||||
assert.Equal(t, "ref-1", updated.ReferenceID)
|
||||
|
||||
_, err = svc.UpdateScoped(ctx, 2, hook.ID, UpdateHookRequest{Status: "enabled"})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_Delete(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil) // nil registry — webhook processing tests are separate
|
||||
@@ -151,6 +209,21 @@ func TestIntegrationHookService_Delete(t *testing.T) {
|
||||
assert.Error(t, err, "should not find deleted hook")
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_DeleteScoped(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
hook, err := svc.Create(ctx, 1, CreateHookRequest{AppID: "webhook"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Error(t, svc.DeleteScoped(ctx, 2, hook.ID))
|
||||
require.NoError(t, svc.DeleteScoped(ctx, 1, hook.ID))
|
||||
|
||||
_, err = svc.Get(ctx, hook.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationHookService_Create_WithInboxID(t *testing.T) {
|
||||
hookRepo, appRepo := setupIntegrationHookTestDB(t)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, nil) // nil registry — webhook processing tests are separate
|
||||
@@ -184,4 +257,4 @@ func TestIntegrationHookService_Create_WithSettings(t *testing.T) {
|
||||
hook, err := svc.Create(ctx, 1, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, hook.Settings)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user