package service import ( "context" "encoding/json" "io" "net/http" "strings" "testing" "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" ) func setupSlackIntegrationServiceTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "failed to open SQLite test database") require.NoError(t, db.AutoMigrate( &model.Account{}, &model.IntegrationHook{}, &model.IntegrationApp{}, ), "failed to auto-migrate models") t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } func setupSlackIntegrationService(t *testing.T) (*SlackIntegrationService, *gorm.DB) { t.Helper() db := setupSlackIntegrationServiceTestDB(t) hookRepo := repository.NewIntegrationHookRepo(db) svc := NewSlackIntegrationService(hookRepo) return svc, db } type slackRoundTripFunc func(*http.Request) (*http.Response, error) func (f slackRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func slackJSONResponse(body string) *http.Response { return &http.Response{ StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), } } func setFakeSlackClient(svc *SlackIntegrationService, rt slackRoundTripFunc) { svc.client = &slackAPIClient{baseURL: "https://slack.test/api", httpClient: &http.Client{Transport: rt}} } func seedSlackAccount(db *gorm.DB, t *testing.T) uint { t.Helper() account := &model.Account{Name: "Test Slack Account"} require.NoError(t, db.Create(account).Error, "failed to seed account") return account.ID } // ======================================== // SlackIntegrationService — Create tests // ======================================== func TestSlackIntegrationService_Create(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) t.Setenv("SLACK_CLIENT_ID", "client-id") t.Setenv("SLACK_CLIENT_SECRET", "client-secret") t.Setenv("FRONTEND_URL", "https://gochat.test") setFakeSlackClient(svc, func(req *http.Request) (*http.Response, error) { assert.Equal(t, "/api/oauth.v2.access", req.URL.Path) assert.Equal(t, http.MethodPost, req.Method) body, _ := io.ReadAll(req.Body) values := string(body) assert.Contains(t, values, "code=oauth-code") assert.Contains(t, values, "redirect_uri=https%3A%2F%2Fgochat.test%2Fapp%2Faccounts%2F1%2Fsettings%2Fintegrations%2Fslack") return slackJSONResponse(`{"ok":true,"access_token":"xoxb-oauth-token"}`), nil }) req := CreateSlackRequest{ Code: "oauth-code", } hook, err := svc.Create(context.Background(), accountID, req) assert.NoError(t, err) assert.NotZero(t, hook.ID) assert.Equal(t, "slack", hook.AppID) assert.Equal(t, model.HookTypeSlack, hook.HookType) assert.Equal(t, model.HookStatusInactive, hook.Status) assert.Equal(t, accountID, hook.AccountID) assert.Equal(t, "xoxb-oauth-token", hook.AccessToken) assert.NotNil(t, hook.Settings) } func TestSlackIntegrationService_Create_SettingsStored(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) req := CreateSlackRequest{ ChannelID: "C99999999", ChannelName: "notifications", SlackToken: "xoxb-special-token", } hook, err := svc.Create(context.Background(), accountID, req) require.NoError(t, err) // Verify that the settings JSON contains the Slack fields var settings model.SlackSettings err = json.Unmarshal(hook.Settings, &settings) require.NoError(t, err, "failed to unmarshal settings JSON") assert.Equal(t, "C99999999", settings.ChannelID) assert.Equal(t, "notifications", settings.ChannelName) assert.Equal(t, "xoxb-special-token", settings.SlackToken) } // ======================================== // SlackIntegrationService — Update tests // ======================================== func TestSlackIntegrationService_Update(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) // Create first createReq := CreateSlackRequest{ ChannelID: "C10000001", ChannelName: "old-channel", SlackToken: "xoxb-old-token", } _, err := svc.Create(context.Background(), accountID, createReq) require.NoError(t, err) // Update setFakeSlackClient(svc, func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/api/conversations.list": if req.URL.Query().Get("types") == "private_channel" { return slackJSONResponse(`{"ok":true,"channels":[],"response_metadata":{"next_cursor":""}}`), nil } return slackJSONResponse(`{"ok":true,"channels":[{"id":"C20000002","name":"new-channel","is_private":false}],"response_metadata":{"next_cursor":""}}`), nil case "/api/conversations.join": return slackJSONResponse(`{"ok":true}`), nil default: t.Fatalf("unexpected Slack API path: %s", req.URL.Path) } return nil, nil }) updateReq := UpdateSlackRequest{ ReferenceID: "C20000002", } updated, err := svc.Update(context.Background(), accountID, updateReq) assert.NoError(t, err) // Verify updated settings var settings model.SlackSettings err = json.Unmarshal(updated.Settings, &settings) require.NoError(t, err) assert.Equal(t, "", settings.ChannelID) assert.Equal(t, "new-channel", settings.ChannelName) assert.Equal(t, "", settings.SlackToken) assert.Equal(t, "C20000002", updated.ReferenceID) assert.Equal(t, model.HookStatusActive, updated.Status) } func TestSlackIntegrationService_Update_NotFound(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) // No Slack hook created for this account — update should fail updateReq := UpdateSlackRequest{ ChannelID: "C00000000", } updated, err := svc.Update(context.Background(), accountID, updateReq) assert.Error(t, err) assert.Nil(t, updated) assert.Contains(t, err.Error(), "not found") } // ======================================== // SlackIntegrationService — Delete tests // ======================================== func TestSlackIntegrationService_Delete(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) // Create first createReq := CreateSlackRequest{ ChannelID: "C30000003", ChannelName: "delete-me", SlackToken: "xoxb-delete-token", } _, err := svc.Create(context.Background(), accountID, createReq) require.NoError(t, err) // Delete err = svc.Delete(context.Background(), accountID) assert.NoError(t, err) // Verify it's gone — no Slack hooks should remain for this account hookRepo := repository.NewIntegrationHookRepo(db) hooks, err := hookRepo.FindByAccountAndType(context.Background(), accountID, model.HookTypeSlack) assert.NoError(t, err) assert.Empty(t, hooks) } func TestSlackIntegrationService_Delete_NotFound(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) // No Slack hook exists for this account — delete should fail err := svc.Delete(context.Background(), accountID) assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } // ======================================== // SlackIntegrationService — ListAllChannels tests // ======================================== func TestSlackIntegrationService_ListAllChannels(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) // Create a Slack integration first createReq := CreateSlackRequest{ ChannelID: "C40000004", ChannelName: "list-channels", SlackToken: "xoxb-list-token", } _, err := svc.Create(context.Background(), accountID, createReq) require.NoError(t, err) setFakeSlackClient(svc, func(req *http.Request) (*http.Response, error) { require.Equal(t, "/api/conversations.list", req.URL.Path) require.Equal(t, "Bearer xoxb-list-token", req.Header.Get("Authorization")) if req.URL.Query().Get("types") == "private_channel" { return slackJSONResponse(`{"ok":true,"channels":[{"id":"G40000004","name":"private-room","is_private":true}],"response_metadata":{"next_cursor":""}}`), nil } return slackJSONResponse(`{"ok":true,"channels":[{"id":"C40000004","name":"list-channels","is_private":false}],"response_metadata":{"next_cursor":""}}`), nil }) channels, err := svc.ListAllChannels(context.Background(), accountID) assert.NoError(t, err) assert.Len(t, channels, 2) assert.Equal(t, "G40000004", channels[0]["id"]) assert.Equal(t, "private-room", channels[0]["name"]) assert.Equal(t, true, channels[0]["is_private"]) assert.Equal(t, "C40000004", channels[1]["id"]) assert.Equal(t, "list-channels", channels[1]["name"]) assert.Equal(t, false, channels[1]["is_private"]) } func TestSlackIntegrationService_ListAllChannels_NotFound(t *testing.T) { svc, db := setupSlackIntegrationService(t) accountID := seedSlackAccount(db, t) // No Slack integration exists for this account channels, err := svc.ListAllChannels(context.Background(), accountID) assert.Error(t, err) assert.Nil(t, channels) assert.Contains(t, err.Error(), "not found") }