Files
gochat/internal/service/slack_integration_service_test.go
T

284 lines
9.0 KiB
Go

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")
}