package v1 import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) type slackHandlerRoundTripFunc func(*http.Request) (*http.Response, error) func (f slackHandlerRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func slackHandlerJSONResponse(body string) *http.Response { return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body))} } func setupSlackIntegrationRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.RedirectTrailingSlash = false handler := NewSlackIntegrationHandler(nil) integrations := r.Group("/api/v1/accounts/:account_id/integrations") RegisterSlackIntegrationRoutes(integrations, handler) return r } func setupSlackIntegrationRouterWithService(svc *service.SlackIntegrationService) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.RedirectTrailingSlash = false handler := NewSlackIntegrationHandler(svc) integrations := r.Group("/api/v1/accounts/:account_id/integrations") RegisterSlackIntegrationRoutes(integrations, handler) return r } func setupSlackIntegrationHandlerDB(t *testing.T) (*gorm.DB, uint) { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.IntegrationHook{})) account := &model.Account{Name: "Slack Handler Account"} require.NoError(t, db.Create(account).Error) t.Cleanup(func() { sqlDB, _ := db.DB() _ = sqlDB.Close() }) return db, account.ID } // ======================================== // SlackIntegration — param validation tests // ======================================== func TestSlackIntegration_Create_BadAccountID(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/integrations/slack/", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) errBody := resp["error"].(map[string]interface{}) assert.Contains(t, errBody["message"], "invalid account_id") } func TestSlackIntegration_Create_InvalidJSON(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/slack/", bytes.NewReader([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.False(t, resp["success"].(bool)) } func TestSlackIntegration_Update_BadAccountID(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/abc/integrations/slack/", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) errBody := resp["error"].(map[string]interface{}) assert.Contains(t, errBody["message"], "invalid account_id") } func TestSlackIntegration_Update_InvalidJSON(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/integrations/slack/", bytes.NewReader([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.False(t, resp["success"].(bool)) } func TestSlackIntegration_Delete_BadAccountID(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/abc/integrations/slack/", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) errBody := resp["error"].(map[string]interface{}) assert.Contains(t, errBody["message"], "invalid account_id") } func TestSlackIntegration_ListAllChannels_BadAccountID(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/integrations/slack/list_all_channels", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) errBody := resp["error"].(map[string]interface{}) assert.Contains(t, errBody["message"], "invalid account_id") } func TestSlackIntegration_Create_NoTrailingSlash_ReturnsRawAppPayload(t *testing.T) { db, accountID := setupSlackIntegrationHandlerDB(t) svc := service.NewSlackIntegrationService(repository.NewIntegrationHookRepo(db)) r := setupSlackIntegrationRouterWithService(svc) w := httptest.NewRecorder() body := bytes.NewReader([]byte(`{"slack_token":"xoxb-handler-token"}`)) req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/slack", body) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "slack", resp["id"]) assert.NotContains(t, resp, "success") hooks := resp["hooks"].([]interface{}) require.Len(t, hooks, 1) hook := hooks[0].(map[string]interface{}) assert.Equal(t, "slack", hook["app_id"]) assert.Equal(t, accountID, uint(hook["account_id"].(float64))) assert.Equal(t, false, hook["status"]) } func TestSlackIntegration_Delete_NoTrailingSlash_ReturnsEmptyOK(t *testing.T) { db, accountID := setupSlackIntegrationHandlerDB(t) require.NoError(t, db.Create(&model.IntegrationHook{AccountID: accountID, AppID: "slack", HookType: model.HookTypeSlack}).Error) svc := service.NewSlackIntegrationService(repository.NewIntegrationHookRepo(db)) r := setupSlackIntegrationRouterWithService(svc) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/integrations/slack", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Empty(t, w.Body.String()) } func TestSlackIntegration_ListAllChannels_NoTrailingSlash_ReturnsChannelArray(t *testing.T) { db, accountID := setupSlackIntegrationHandlerDB(t) require.NoError(t, db.Create(&model.IntegrationHook{AccountID: accountID, AppID: "slack", HookType: model.HookTypeSlack, AccessToken: "xoxb-handler-token"}).Error) svc := service.NewSlackIntegrationService(repository.NewIntegrationHookRepo(db), service.WithSlackHTTPClient("https://slack.test/api", &http.Client{Transport: slackHandlerRoundTripFunc(func(req *http.Request) (*http.Response, error) { require.Equal(t, "/api/conversations.list", req.URL.Path) require.Equal(t, "Bearer xoxb-handler-token", req.Header.Get("Authorization")) if req.URL.Query().Get("types") == "private_channel" { return slackHandlerJSONResponse(`{"ok":true,"channels":[{"id":"G1","name":"private-room","is_private":true}],"response_metadata":{"next_cursor":""}}`), nil } return slackHandlerJSONResponse(`{"ok":true,"channels":[{"id":"C1","name":"support","is_private":false}],"response_metadata":{"next_cursor":""}}`), nil })})) r := setupSlackIntegrationRouterWithService(svc) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/slack/list_all_channels", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp []map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) require.Len(t, resp, 2) assert.Equal(t, "G1", resp[0]["id"]) assert.Equal(t, "private-room", resp[0]["name"]) assert.Equal(t, true, resp[0]["is_private"]) assert.Equal(t, "C1", resp[1]["id"]) assert.Equal(t, "support", resp[1]["name"]) assert.Equal(t, false, resp[1]["is_private"]) } func TestSlackIntegration_Update_PutNoTrailingSlash_BadAccountID(t *testing.T) { r := setupSlackIntegrationRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/accounts/abc/integrations/slack", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) }