mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
feat(go-api): complete chat channel API migration with tests (#16139)
close #16132 ## Summary This PR completes the Go-side merge and cleanup for chat channel APIs, including handler/service wiring, route registration, and test coverage. Implemented and aligned 5 chat channel APIs: ``` - POST `/api/v1/chat-channels` - GET `/api/v1/chat-channels` - GET `/api/v1/chat-channels/:channel_id` - PATCH `/api/v1/chat-channels/:channel_id` - DELETE `/api/v1/chat-channels/:channel_id` ``` Co-authored-by: Haruko386 <tryeverypossible@163.com>
This commit is contained in:
@@ -212,6 +212,7 @@ func startServer(config *server.Config) {
|
||||
llmService := service.NewLLMService()
|
||||
tenantService := service.NewTenantService()
|
||||
chatService := service.NewChatService()
|
||||
chatChannelService := service.NewChatChannelService()
|
||||
chatSessionService := service.NewChatSessionService()
|
||||
openaiChatService := service.NewOpenAIChatService()
|
||||
systemService := service.NewSystemService()
|
||||
@@ -236,6 +237,7 @@ func startServer(config *server.Config) {
|
||||
chunkHandler := handler.NewChunkHandler(chunkService, userService)
|
||||
llmHandler := handler.NewLLMHandler(llmService, userService)
|
||||
chatHandler := handler.NewChatHandler(chatService, userService)
|
||||
chatChannelHandler := handler.NewChatChannelHandler(chatChannelService)
|
||||
chatSessionHandler := handler.NewChatSessionHandler(chatSessionService, userService)
|
||||
openaiChatHandler := handler.NewOpenAIChatHandler(openaiChatService)
|
||||
connectorHandler := handler.NewConnectorHandler(connectorService, userService)
|
||||
@@ -292,7 +294,6 @@ func startServer(config *server.Config) {
|
||||
docDAO,
|
||||
docEngine,
|
||||
)
|
||||
|
||||
// Per-tenant canvas-runtime override selector, backed by the
|
||||
// existing Redis client and the global logger. The handler is
|
||||
// ALWAYS constructed, even when Redis is briefly unavailable at
|
||||
@@ -310,7 +311,7 @@ func startServer(config *server.Config) {
|
||||
adminRuntimeHandler := handler.NewAdminRuntimeHandler(adminRuntimeSelector)
|
||||
|
||||
// Initialize router
|
||||
r := router.NewRouter(authHandler, userHandler, tenantHandler, documentHandler, datasetsHandler, systemHandler, knowledgebaseHandler, chunkHandler, llmHandler, chatHandler, chatSessionHandler, connectorHandler, searchHandler, fileHandler, memoryHandler, mcpHandler, skillSearchHandler, providerHandler, agentHandler, searchBotHandler, difyRetrievalHandler, pluginHandler, modelHandler, fileCommitHandler, adminRuntimeHandler, openaiChatHandler)
|
||||
r := router.NewRouter(authHandler, userHandler, tenantHandler, documentHandler, datasetsHandler, systemHandler, knowledgebaseHandler, chunkHandler, llmHandler, chatHandler, chatChannelHandler, chatSessionHandler, connectorHandler, searchHandler, fileHandler, memoryHandler, mcpHandler, skillSearchHandler, providerHandler, agentHandler, searchBotHandler, difyRetrievalHandler, pluginHandler, modelHandler, fileCommitHandler, adminRuntimeHandler, openaiChatHandler)
|
||||
|
||||
// Create Gin engine
|
||||
ginEngine := gin.New()
|
||||
|
||||
@@ -12,6 +12,15 @@ func (dao *ChatChannelDAO) Create(channel *entity.ChatChannel) error {
|
||||
return DB.Create(channel).Error
|
||||
}
|
||||
|
||||
func (dao *ChatChannelDAO) GetByIDOnly(id string) (*entity.ChatChannel, error) {
|
||||
var channel entity.ChatChannel
|
||||
err := DB.Where("id = ?", id).First(&channel).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, err
|
||||
}
|
||||
|
||||
func (dao *ChatChannelDAO) GetByID(id string, tenantID string) (*entity.ChatChannel, error) {
|
||||
var channel entity.ChatChannel
|
||||
err := DB.Where("id = ? AND tenant_id = ?", id, tenantID).First(&channel).Error
|
||||
@@ -21,14 +30,17 @@ func (dao *ChatChannelDAO) GetByID(id string, tenantID string) (*entity.ChatChan
|
||||
return &channel, err
|
||||
}
|
||||
|
||||
// UpdateByID Update a single record by ID
|
||||
func (dao *ChatChannelDAO) UpdateByID(id string, tenantID string, updates map[string]any) error {
|
||||
return DB.Model(&entity.ChatChannel{}).Where("id = ? AND tenant_id = ?", id, tenantID).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteByID Delete a single record by ID
|
||||
func (dao *ChatChannelDAO) DeleteByID(id string, tenantID string) error {
|
||||
return DB.Where("id = ? AND tenant_id = ?", id, tenantID).Delete(&entity.ChatChannel{}).Error
|
||||
}
|
||||
|
||||
// ListByTenantID List a single record by TenantID
|
||||
func (dao *ChatChannelDAO) ListByTenantID(tenantID string) ([]*entity.ChatChannelListResponse, error) {
|
||||
results := make([]*entity.ChatChannelListResponse, 0)
|
||||
|
||||
|
||||
229
internal/handler/chat_channel.go
Normal file
229
internal/handler/chat_channel.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/entity"
|
||||
"ragflow/internal/service"
|
||||
)
|
||||
|
||||
type ChatChannelService interface {
|
||||
CreateChatChannel(tenantID, name, channelType string, config entity.JSONMap, chatID *string) (*entity.ChatChannel, error)
|
||||
List(tenantID string) ([]*entity.ChatChannelListResponse, error)
|
||||
GetChatChannel(userID, channelID string) (*entity.ChatChannel, common.ErrorCode, error)
|
||||
UpdateChatChannel(userID, channelID string, req map[string]interface{}) (*entity.ChatChannel, common.ErrorCode, error)
|
||||
DeleteChatChannel(userID, channelID string) (bool, common.ErrorCode, error)
|
||||
}
|
||||
|
||||
type ChatChannelHandler struct {
|
||||
chatChannelService ChatChannelService
|
||||
}
|
||||
|
||||
func NewChatChannelHandler(chatChannelService ChatChannelService) *ChatChannelHandler {
|
||||
return &ChatChannelHandler{chatChannelService: chatChannelService}
|
||||
}
|
||||
|
||||
// NewChatChannel keeps the existing constructor shape used by boot code.
|
||||
func NewChatChannel() *ChatChannelHandler {
|
||||
return NewChatChannelHandler(service.NewChatChannelService())
|
||||
}
|
||||
|
||||
type CreateChatChannelRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required"`
|
||||
Config entity.JSONMap `json:"config" binding:"required"`
|
||||
ChatID *string `json:"chat_id"`
|
||||
}
|
||||
|
||||
// CreateChatChannel handles POST /chat-channels.
|
||||
func (h *ChatChannelHandler) CreateChatChannel(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateChatChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
jsonError(c, common.CodeDataError, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
row, err := h.chatChannelService.CreateChatChannel(
|
||||
user.ID,
|
||||
req.Name,
|
||||
req.Channel,
|
||||
req.Config,
|
||||
req.ChatID,
|
||||
)
|
||||
if err != nil {
|
||||
jsonError(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(c, common.CodeSuccess, row, "success")
|
||||
}
|
||||
|
||||
// ListChatChannel handles GET /chat-channels.
|
||||
func (h *ChatChannelHandler) ListChatChannel(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.chatChannelService.List(user.ID)
|
||||
if err != nil {
|
||||
jsonError(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(c, common.CodeSuccess, rows, "success")
|
||||
}
|
||||
|
||||
// GetChatChannel handles GET /chat-channels/:channel_id.
|
||||
func (h *ChatChannelHandler) GetChatChannel(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
userID := strings.TrimSpace(user.ID)
|
||||
if userID == "" {
|
||||
jsonError(c, common.CodeArgumentError, "user_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
channelID := strings.TrimSpace(c.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
jsonError(c, common.CodeArgumentError, "channel_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
channel, code, err := h.chatChannelService.GetChatChannel(userID, channelID)
|
||||
if code != common.CodeSuccess || err != nil {
|
||||
writeChatChannelError(c, code, chatChannelErrMsg(code, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": channel,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateChatChannel handles PATCH /chat-channels/:channel_id.
|
||||
func (h *ChatChannelHandler) UpdateChatChannel(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
userID := strings.TrimSpace(user.ID)
|
||||
if userID == "" {
|
||||
jsonError(c, common.CodeArgumentError, "user_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
channelID := strings.TrimSpace(c.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
jsonError(c, common.CodeArgumentError, "channel_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
var request map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
jsonError(c, common.CodeDataError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, code, err := h.chatChannelService.UpdateChatChannel(userID, channelID, unwrapChatChannelPayload(request))
|
||||
if code != common.CodeSuccess || err != nil {
|
||||
writeChatChannelError(c, code, chatChannelErrMsg(code, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": result,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteChatChannel handles DELETE /chat-channels/:channel_id.
|
||||
func (h *ChatChannelHandler) DeleteChatChannel(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
userID := strings.TrimSpace(user.ID)
|
||||
if userID == "" {
|
||||
jsonError(c, common.CodeArgumentError, "user_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
channelID := strings.TrimSpace(c.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
jsonError(c, common.CodeArgumentError, "channel_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
result, code, err := h.chatChannelService.DeleteChatChannel(userID, channelID)
|
||||
if code != common.CodeSuccess || err != nil {
|
||||
writeChatChannelError(c, code, chatChannelErrMsg(code, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": result,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
func unwrapChatChannelPayload(payload map[string]interface{}) map[string]interface{} {
|
||||
if data, ok := payload["data"].(map[string]interface{}); ok {
|
||||
return data
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeChatChannelError(c *gin.Context, code common.ErrorCode, message string) {
|
||||
if code == common.CodeAuthenticationError && message == "No authorization." {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"data": false,
|
||||
"message": message,
|
||||
})
|
||||
return
|
||||
}
|
||||
jsonError(c, code, message)
|
||||
}
|
||||
|
||||
func chatChannelErrMsg(code common.ErrorCode, err error) string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return code.Message()
|
||||
}
|
||||
328
internal/handler/chat_channel_test.go
Normal file
328
internal/handler/chat_channel_test.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/entity"
|
||||
)
|
||||
|
||||
type fakeChatChannelService struct {
|
||||
createFn func(tenantID, name, channelType string, config entity.JSONMap, chatID *string) (*entity.ChatChannel, error)
|
||||
listFn func(tenantID string) ([]*entity.ChatChannelListResponse, error)
|
||||
getFn func(userID, channelID string) (*entity.ChatChannel, common.ErrorCode, error)
|
||||
updateFn func(userID, channelID string, req map[string]interface{}) (*entity.ChatChannel, common.ErrorCode, error)
|
||||
deleteFn func(userID, channelID string) (bool, common.ErrorCode, error)
|
||||
}
|
||||
|
||||
func (f fakeChatChannelService) CreateChatChannel(tenantID, name, channelType string, config entity.JSONMap, chatID *string) (*entity.ChatChannel, error) {
|
||||
if f.createFn == nil {
|
||||
return nil, errors.New("unexpected CreateChatChannel call")
|
||||
}
|
||||
return f.createFn(tenantID, name, channelType, config, chatID)
|
||||
}
|
||||
|
||||
func (f fakeChatChannelService) List(tenantID string) ([]*entity.ChatChannelListResponse, error) {
|
||||
if f.listFn == nil {
|
||||
return nil, errors.New("unexpected List call")
|
||||
}
|
||||
return f.listFn(tenantID)
|
||||
}
|
||||
|
||||
func (f fakeChatChannelService) GetChatChannel(userID, channelID string) (*entity.ChatChannel, common.ErrorCode, error) {
|
||||
if f.getFn == nil {
|
||||
return nil, common.CodeServerError, errors.New("unexpected GetChatChannel call")
|
||||
}
|
||||
return f.getFn(userID, channelID)
|
||||
}
|
||||
|
||||
func (f fakeChatChannelService) UpdateChatChannel(userID, channelID string, req map[string]interface{}) (*entity.ChatChannel, common.ErrorCode, error) {
|
||||
if f.updateFn == nil {
|
||||
return nil, common.CodeServerError, errors.New("unexpected UpdateChatChannel call")
|
||||
}
|
||||
return f.updateFn(userID, channelID, req)
|
||||
}
|
||||
|
||||
func (f fakeChatChannelService) DeleteChatChannel(userID, channelID string) (bool, common.ErrorCode, error) {
|
||||
if f.deleteFn == nil {
|
||||
return false, common.CodeServerError, errors.New("unexpected DeleteChatChannel call")
|
||||
}
|
||||
return f.deleteFn(userID, channelID)
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerCreateSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
var gotTenantID, gotName, gotChannel string
|
||||
var gotConfig entity.JSONMap
|
||||
var gotChatID *string
|
||||
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
createFn: func(tenantID, name, channelType string, config entity.JSONMap, chatID *string) (*entity.ChatChannel, error) {
|
||||
gotTenantID = tenantID
|
||||
gotName = name
|
||||
gotChannel = channelType
|
||||
gotConfig = config
|
||||
gotChatID = chatID
|
||||
return &entity.ChatChannel{
|
||||
ID: "cc-1",
|
||||
TenantID: tenantID,
|
||||
Name: name,
|
||||
Channel: channelType,
|
||||
Config: config,
|
||||
ChatID: chatID,
|
||||
Status: 1,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/chat-channels", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.CreateChatChannel(c)
|
||||
})
|
||||
|
||||
body := `{"name":"bot-a","channel":"dingtalk","config":{"token":"abc"},"chat_id":"dialog-1"}`
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat-channels", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
|
||||
if gotTenantID != "tenant-1" || gotName != "bot-a" || gotChannel != "dingtalk" {
|
||||
t.Fatalf("service args tenant=%q name=%q channel=%q", gotTenantID, gotName, gotChannel)
|
||||
}
|
||||
if gotConfig["token"] != "abc" {
|
||||
t.Fatalf("config=%v", gotConfig)
|
||||
}
|
||||
if gotChatID == nil || *gotChatID != "dialog-1" {
|
||||
t.Fatalf("chatID=%v", gotChatID)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if payload["code"] != float64(common.CodeSuccess) {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerCreateInvalidRequestStopsEarly(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
called := false
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
createFn: func(tenantID, name, channelType string, config entity.JSONMap, chatID *string) (*entity.ChatChannel, error) {
|
||||
called = true
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/chat-channels", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.CreateChatChannel(c)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat-channels", strings.NewReader(`{}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
if called {
|
||||
t.Fatal("service should not be called when request binding fails")
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if payload["code"] != float64(common.CodeDataError) {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerListSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
gotTenantID := ""
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
listFn: func(tenantID string) ([]*entity.ChatChannelListResponse, error) {
|
||||
gotTenantID = tenantID
|
||||
return []*entity.ChatChannelListResponse{
|
||||
{ID: "cc-1", Name: "bot-a", Channel: "dingtalk", Status: 1},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/chat-channels", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.ListChatChannel(c)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/chat-channels", nil)
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
if gotTenantID != "tenant-1" {
|
||||
t.Fatalf("tenantID=%q", gotTenantID)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if payload["code"] != float64(common.CodeSuccess) {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerListServiceError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
listFn: func(tenantID string) ([]*entity.ChatChannelListResponse, error) {
|
||||
return nil, errors.New("db failed")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/chat-channels", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.ListChatChannel(c)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/chat-channels", nil)
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if payload["code"] != float64(common.CodeServerError) {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerGetChatChannelUnauthorized(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
getFn: func(userID, channelID string) (*entity.ChatChannel, common.ErrorCode, error) {
|
||||
return nil, common.CodeAuthenticationError, errors.New("No authorization.")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/chat-channels/:channel_id", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.GetChatChannel(c)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/chat-channels/cc-1", nil)
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if payload["code"] != float64(common.CodeAuthenticationError) {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
if payload["data"] != false {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerUpdateChatChannelUnwrapsData(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
var gotReq map[string]interface{}
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
updateFn: func(userID, channelID string, req map[string]interface{}) (*entity.ChatChannel, common.ErrorCode, error) {
|
||||
gotReq = req
|
||||
return &entity.ChatChannel{ID: channelID, TenantID: userID, Name: "new-name"}, common.CodeSuccess, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.PATCH("/api/v1/chat-channels/:channel_id", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.UpdateChatChannel(c)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/chat-channels/cc-1", strings.NewReader(`{"data":{"name":"new-name"}}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
if gotReq["name"] != "new-name" {
|
||||
t.Fatalf("req=%v", gotReq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelHandlerDeleteChatChannelSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
var gotUserID, gotChannelID string
|
||||
h := &ChatChannelHandler{
|
||||
chatChannelService: fakeChatChannelService{
|
||||
deleteFn: func(userID, channelID string) (bool, common.ErrorCode, error) {
|
||||
gotUserID = userID
|
||||
gotChannelID = channelID
|
||||
return true, common.CodeSuccess, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.DELETE("/api/v1/chat-channels/:channel_id", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "tenant-1"})
|
||||
h.DeleteChatChannel(c)
|
||||
})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/chat-channels/cc-1", nil)
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
if gotUserID != "tenant-1" || gotChannelID != "cc-1" {
|
||||
t.Fatalf("userID=%q channelID=%q", gotUserID, gotChannelID)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if payload["code"] != float64(common.CodeSuccess) {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
if payload["data"] != true {
|
||||
t.Fatalf("payload=%v", payload)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type Router struct {
|
||||
chunkHandler *handler.ChunkHandler
|
||||
llmHandler *handler.LLMHandler
|
||||
chatHandler *handler.ChatHandler
|
||||
chatChannelHandler *handler.ChatChannelHandler
|
||||
openaiChatHandler *handler.OpenAIChatHandler
|
||||
chatSessionHandler *handler.ChatSessionHandler
|
||||
connectorHandler *handler.ConnectorHandler
|
||||
@@ -63,6 +64,7 @@ func NewRouter(
|
||||
chunkHandler *handler.ChunkHandler,
|
||||
llmHandler *handler.LLMHandler,
|
||||
chatHandler *handler.ChatHandler,
|
||||
chatChannelHandler *handler.ChatChannelHandler,
|
||||
chatSessionHandler *handler.ChatSessionHandler,
|
||||
connectorHandler *handler.ConnectorHandler,
|
||||
searchHandler *handler.SearchHandler,
|
||||
@@ -91,6 +93,7 @@ func NewRouter(
|
||||
chunkHandler: chunkHandler,
|
||||
llmHandler: llmHandler,
|
||||
chatHandler: chatHandler,
|
||||
chatChannelHandler: chatChannelHandler,
|
||||
openaiChatHandler: openaiChatHandler,
|
||||
chatSessionHandler: chatSessionHandler,
|
||||
connectorHandler: connectorHandler,
|
||||
@@ -608,6 +611,16 @@ func (r *Router) Setup(engine *gin.Engine) {
|
||||
chat.POST("/rm", r.chatHandler.RemoveChats)
|
||||
}
|
||||
|
||||
// Chat Channel
|
||||
chanChannel := v1.Group("/chat-channels")
|
||||
{
|
||||
chanChannel.POST("", r.chatChannelHandler.CreateChatChannel)
|
||||
chanChannel.GET("", r.chatChannelHandler.ListChatChannel)
|
||||
chanChannel.GET("/:channel_id", r.chatChannelHandler.GetChatChannel)
|
||||
chanChannel.PATCH("/:channel_id", r.chatChannelHandler.UpdateChatChannel)
|
||||
chanChannel.DELETE("/:channel_id", r.chatChannelHandler.DeleteChatChannel)
|
||||
}
|
||||
|
||||
// Chat session (conversation) routes
|
||||
session := authorized.Group("/v1/conversation")
|
||||
{
|
||||
|
||||
233
internal/service/chat_channel.go
Normal file
233
internal/service/chat_channel.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/dao"
|
||||
"ragflow/internal/entity"
|
||||
)
|
||||
|
||||
type ChatChannelService struct {
|
||||
chatChannelDAO *dao.ChatChannelDAO
|
||||
chatDAO *dao.ChatDAO
|
||||
userTenantDAO *dao.UserTenantDAO
|
||||
}
|
||||
|
||||
func NewChatChannelService() *ChatChannelService {
|
||||
return &ChatChannelService{
|
||||
chatChannelDAO: dao.NewChatChannel(),
|
||||
chatDAO: dao.NewChatDAO(),
|
||||
userTenantDAO: dao.NewUserTenantDAO(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) Insert(channel *entity.ChatChannel) error {
|
||||
if channel == nil {
|
||||
return errors.New("channel is nil")
|
||||
}
|
||||
if channel.ID == "" {
|
||||
channel.ID = common.GenerateUUID()
|
||||
}
|
||||
if channel.Status == 0 {
|
||||
channel.Status = 1
|
||||
}
|
||||
return s.chatChannelDAO.Create(channel)
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) GetByID(id string) (*entity.ChatChannel, error) {
|
||||
if id == "" {
|
||||
return nil, errors.New("id is empty")
|
||||
}
|
||||
return s.chatChannelDAO.GetByIDOnly(id)
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) List(tenantID string) ([]*entity.ChatChannelListResponse, error) {
|
||||
return s.chatChannelDAO.ListByTenantID(tenantID)
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) CreateChatChannel(tenantID, name, channelType string, config entity.JSONMap, chatID *string) (*entity.ChatChannel, error) {
|
||||
if chatID != nil && *chatID != "" {
|
||||
dialog, err := s.chatDAO.GetByID(*chatID)
|
||||
if err != nil {
|
||||
if dao.IsNotFoundErr(err) {
|
||||
return nil, errors.New("Can't find this chat assistant!")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if dialog.TenantID != tenantID {
|
||||
return nil, errors.New("No authorization.")
|
||||
}
|
||||
}
|
||||
row := &entity.ChatChannel{
|
||||
ID: common.GenerateUUID(),
|
||||
TenantID: tenantID,
|
||||
Name: name,
|
||||
Channel: channelType,
|
||||
Config: config,
|
||||
ChatID: chatID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := s.Insert(row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created, err := s.GetByID(row.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load created chat channel: %w", err)
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) accessible(userID, channelID string) (*entity.ChatChannel, bool, error) {
|
||||
channel, err := s.chatChannelDAO.GetByIDOnly(channelID)
|
||||
if err != nil {
|
||||
if dao.IsNotFoundErr(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if channel.TenantID == userID {
|
||||
return channel, true, nil
|
||||
}
|
||||
|
||||
tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
for _, tenantID := range tenantIDs {
|
||||
if tenantID == channel.TenantID {
|
||||
return channel, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return channel, false, nil
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) GetChatChannel(userID, channelID string) (*entity.ChatChannel, common.ErrorCode, error) {
|
||||
_, ok, err := s.accessible(userID, channelID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, common.CodeAuthenticationError, errors.New("No authorization.")
|
||||
}
|
||||
|
||||
channel, err := s.chatChannelDAO.GetByIDOnly(channelID)
|
||||
if err != nil {
|
||||
if dao.IsNotFoundErr(err) {
|
||||
return nil, common.CodeDataError, errors.New("Can't find this chat channel!")
|
||||
}
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
return channel, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) UpdateChatChannel(userID, channelID string, req map[string]interface{}) (*entity.ChatChannel, common.ErrorCode, error) {
|
||||
channel, ok, err := s.accessible(userID, channelID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, common.CodeAuthenticationError, errors.New("No authorization.")
|
||||
}
|
||||
if channel == nil {
|
||||
return nil, common.CodeDataError, errors.New("Can't find this chat channel!")
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
|
||||
if value, exists := req["name"]; exists {
|
||||
name, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, common.CodeDataError, errors.New("name must be string")
|
||||
}
|
||||
updates["name"] = name
|
||||
}
|
||||
|
||||
if value, exists := req["config"]; exists {
|
||||
if value == nil {
|
||||
updates["config"] = nil
|
||||
} else {
|
||||
config, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, common.CodeDataError, errors.New("config must be object")
|
||||
}
|
||||
updates["config"] = entity.JSONMap(config)
|
||||
}
|
||||
}
|
||||
|
||||
if value, exists := req["chat_id"]; exists {
|
||||
if value == nil {
|
||||
updates["chat_id"] = nil
|
||||
} else {
|
||||
chatID, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, common.CodeDataError, errors.New("chat_id must be string or null")
|
||||
}
|
||||
if chatID != "" {
|
||||
dialog, err := s.chatDAO.GetByID(chatID)
|
||||
if err != nil {
|
||||
if dao.IsNotFoundErr(err) {
|
||||
return nil, common.CodeDataError, errors.New("Can't find this chat assistant!")
|
||||
}
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
if dialog.TenantID != channel.TenantID {
|
||||
return nil, common.CodeAuthenticationError, errors.New("No authorization.")
|
||||
}
|
||||
}
|
||||
updates["chat_id"] = chatID
|
||||
}
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := s.chatChannelDAO.UpdateByID(channelID, channel.TenantID, updates); err != nil {
|
||||
return nil, common.CodeDataError, err
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := s.chatChannelDAO.GetByIDOnly(channelID)
|
||||
if err != nil {
|
||||
if dao.IsNotFoundErr(err) {
|
||||
return nil, common.CodeDataError, errors.New("Can't find this chat channel!")
|
||||
}
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
return updated, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
func (s *ChatChannelService) DeleteChatChannel(userID, channelID string) (bool, common.ErrorCode, error) {
|
||||
channel, ok, err := s.accessible(userID, channelID)
|
||||
if err != nil {
|
||||
return false, common.CodeServerError, err
|
||||
}
|
||||
if !ok {
|
||||
return false, common.CodeAuthenticationError, errors.New("No authorization.")
|
||||
}
|
||||
if channel == nil {
|
||||
return false, common.CodeAuthenticationError, errors.New("No authorization.")
|
||||
}
|
||||
|
||||
if err := s.chatChannelDAO.DeleteByID(channelID, channel.TenantID); err != nil {
|
||||
return false, common.CodeDataError, err
|
||||
}
|
||||
return true, common.CodeSuccess, nil
|
||||
}
|
||||
238
internal/service/chat_channel_test.go
Normal file
238
internal/service/chat_channel_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/dao"
|
||||
"ragflow/internal/entity"
|
||||
)
|
||||
|
||||
func setupChatChannelServiceTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
TranslateError: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open sqlite: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&entity.ChatChannel{}, &entity.Chat{}, &entity.UserTenant{}); err != nil {
|
||||
t.Fatalf("failed to migrate test schema: %v", err)
|
||||
}
|
||||
|
||||
origDB := dao.DB
|
||||
dao.DB = db
|
||||
t.Cleanup(func() {
|
||||
dao.DB = origDB
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func createServiceTestDialog(t *testing.T, db *gorm.DB, id, tenantID, name string) *entity.Chat {
|
||||
t.Helper()
|
||||
|
||||
dialogName := name
|
||||
dialog := &entity.Chat{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
Name: &dialogName,
|
||||
LLMID: "model-a",
|
||||
LLMSetting: entity.JSONMap{"temperature": 0.1},
|
||||
PromptType: "simple",
|
||||
PromptConfig: entity.JSONMap{"system": "sys"},
|
||||
KBIDs: entity.JSONSlice{"kb-1"},
|
||||
}
|
||||
if err := db.Create(dialog).Error; err != nil {
|
||||
t.Fatalf("failed to create dialog: %v", err)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
func createServiceTestChannel(t *testing.T, db *gorm.DB, channel *entity.ChatChannel) *entity.ChatChannel {
|
||||
t.Helper()
|
||||
|
||||
if err := db.Create(channel).Error; err != nil {
|
||||
t.Fatalf("failed to create chat channel: %v", err)
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
||||
func createServiceTestMembership(t *testing.T, db *gorm.DB, userID, tenantID string) {
|
||||
t.Helper()
|
||||
|
||||
status := "1"
|
||||
member := &entity.UserTenant{
|
||||
ID: userID + "_" + tenantID,
|
||||
UserID: userID,
|
||||
TenantID: tenantID,
|
||||
Role: "normal",
|
||||
Status: &status,
|
||||
}
|
||||
if err := db.Create(member).Error; err != nil {
|
||||
t.Fatalf("failed to create tenant membership: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelServiceCreateAndList(t *testing.T) {
|
||||
db := setupChatChannelServiceTestDB(t)
|
||||
createServiceTestDialog(t, db, "dialog-1", "tenant-1", "Assistant A")
|
||||
|
||||
svc := NewChatChannelService()
|
||||
chatID := "dialog-1"
|
||||
|
||||
channel, err := svc.CreateChatChannel(
|
||||
"tenant-1",
|
||||
"bot-a",
|
||||
"dingtalk",
|
||||
entity.JSONMap{"token": "abc"},
|
||||
&chatID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChatChannel failed: %v", err)
|
||||
}
|
||||
if channel.ID == "" {
|
||||
t.Fatal("expected generated id")
|
||||
}
|
||||
if channel.TenantID != "tenant-1" || channel.Name != "bot-a" || channel.Channel != "dingtalk" {
|
||||
t.Fatalf("unexpected created channel: %+v", channel)
|
||||
}
|
||||
|
||||
list, err := svc.List("tenant-1")
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("expected 1 channel, got %d", len(list))
|
||||
}
|
||||
if list[0].DialogName == nil || *list[0].DialogName != "Assistant A" {
|
||||
t.Fatalf("expected joined dialog name, got %+v", list[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelServiceGetChatChannelAllowsJoinedTenant(t *testing.T) {
|
||||
db := setupChatChannelServiceTestDB(t)
|
||||
createServiceTestChannel(t, db, &entity.ChatChannel{
|
||||
ID: "cc-1",
|
||||
TenantID: "tenant-1",
|
||||
Name: "bot-a",
|
||||
Channel: "wecom",
|
||||
Config: entity.JSONMap{"token": "abc"},
|
||||
Status: 1,
|
||||
})
|
||||
createServiceTestMembership(t, db, "user-2", "tenant-1")
|
||||
|
||||
channel, code, err := NewChatChannelService().GetChatChannel("user-2", "cc-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetChatChannel failed: %v", err)
|
||||
}
|
||||
if code != common.CodeSuccess {
|
||||
t.Fatalf("expected success code, got %v", code)
|
||||
}
|
||||
if channel == nil || channel.ID != "cc-1" {
|
||||
t.Fatalf("unexpected channel: %+v", channel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelServiceUpdateChatChannelSuccess(t *testing.T) {
|
||||
db := setupChatChannelServiceTestDB(t)
|
||||
createServiceTestDialog(t, db, "dialog-1", "tenant-1", "Assistant A")
|
||||
createServiceTestDialog(t, db, "dialog-2", "tenant-1", "Assistant B")
|
||||
chatID := "dialog-1"
|
||||
createServiceTestChannel(t, db, &entity.ChatChannel{
|
||||
ID: "cc-1",
|
||||
TenantID: "tenant-1",
|
||||
Name: "bot-a",
|
||||
Channel: "wecom",
|
||||
Config: entity.JSONMap{"token": "old"},
|
||||
ChatID: &chatID,
|
||||
Status: 1,
|
||||
})
|
||||
|
||||
updated, code, err := NewChatChannelService().UpdateChatChannel("tenant-1", "cc-1", map[string]interface{}{
|
||||
"name": "bot-b",
|
||||
"config": map[string]interface{}{"token": "new"},
|
||||
"chat_id": "dialog-2",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateChatChannel failed: %v", err)
|
||||
}
|
||||
if code != common.CodeSuccess {
|
||||
t.Fatalf("expected success code, got %v", code)
|
||||
}
|
||||
if updated.Name != "bot-b" {
|
||||
t.Fatalf("expected updated name, got %+v", updated)
|
||||
}
|
||||
if updated.ChatID == nil || *updated.ChatID != "dialog-2" {
|
||||
t.Fatalf("expected updated chat_id, got %+v", updated.ChatID)
|
||||
}
|
||||
if updated.Config["token"] != "new" {
|
||||
t.Fatalf("expected updated config, got %+v", updated.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelServiceUpdateChatChannelRejectsCrossTenantDialog(t *testing.T) {
|
||||
db := setupChatChannelServiceTestDB(t)
|
||||
createServiceTestDialog(t, db, "dialog-2", "tenant-2", "Assistant B")
|
||||
createServiceTestChannel(t, db, &entity.ChatChannel{
|
||||
ID: "cc-1",
|
||||
TenantID: "tenant-1",
|
||||
Name: "bot-a",
|
||||
Channel: "wecom",
|
||||
Config: entity.JSONMap{"token": "old"},
|
||||
Status: 1,
|
||||
})
|
||||
|
||||
_, code, err := NewChatChannelService().UpdateChatChannel("tenant-1", "cc-1", map[string]interface{}{
|
||||
"chat_id": "dialog-2",
|
||||
})
|
||||
if code != common.CodeAuthenticationError {
|
||||
t.Fatalf("expected authentication error, got %v", code)
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), "No authorization.") {
|
||||
t.Fatalf("expected authorization error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatChannelServiceDeleteChatChannel(t *testing.T) {
|
||||
db := setupChatChannelServiceTestDB(t)
|
||||
createServiceTestChannel(t, db, &entity.ChatChannel{
|
||||
ID: "cc-1",
|
||||
TenantID: "tenant-1",
|
||||
Name: "bot-a",
|
||||
Channel: "wecom",
|
||||
Config: entity.JSONMap{"token": "old"},
|
||||
Status: 1,
|
||||
})
|
||||
|
||||
svc := NewChatChannelService()
|
||||
|
||||
deleted, code, err := svc.DeleteChatChannel("user-2", "cc-1")
|
||||
if code != common.CodeAuthenticationError {
|
||||
t.Fatalf("expected authentication error, got %v", code)
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), "No authorization.") {
|
||||
t.Fatalf("expected authorization error, got %v", err)
|
||||
}
|
||||
if deleted {
|
||||
t.Fatal("expected delete to be rejected")
|
||||
}
|
||||
|
||||
deleted, code, err = svc.DeleteChatChannel("tenant-1", "cc-1")
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteChatChannel failed: %v", err)
|
||||
}
|
||||
if code != common.CodeSuccess || !deleted {
|
||||
t.Fatalf("unexpected delete result: deleted=%v code=%v err=%v", deleted, code, err)
|
||||
}
|
||||
|
||||
if _, err := svc.GetByID("cc-1"); err == nil {
|
||||
t.Fatal("expected deleted record to be gone")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user