Files
picoclaw/pkg/channels/slack/slack_test.go
T
2026-05-22 16:28:28 -07:00

265 lines
6.8 KiB
Go

package slack
import (
"context"
"os"
"path/filepath"
"testing"
slacksdk "github.com/slack-go/slack"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/media"
)
func TestParseSlackChatID(t *testing.T) {
tests := []struct {
name string
chatID string
wantChanID string
wantThread string
}{
{
name: "channel only",
chatID: "C123456",
wantChanID: "C123456",
wantThread: "",
},
{
name: "channel with thread",
chatID: "C123456/1234567890.123456",
wantChanID: "C123456",
wantThread: "1234567890.123456",
},
{
name: "DM channel",
chatID: "D987654",
wantChanID: "D987654",
wantThread: "",
},
{
name: "empty string",
chatID: "",
wantChanID: "",
wantThread: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
chanID, threadTS := parseSlackChatID(tt.chatID)
if chanID != tt.wantChanID {
t.Errorf("parseSlackChatID(%q) channelID = %q, want %q", tt.chatID, chanID, tt.wantChanID)
}
if threadTS != tt.wantThread {
t.Errorf("parseSlackChatID(%q) threadTS = %q, want %q", tt.chatID, threadTS, tt.wantThread)
}
})
}
}
func TestResolveSlackOutboundTarget_PrefersContextTopicID(t *testing.T) {
deliveryChatID, channelID, threadTS := resolveSlackOutboundTarget("C123456", &bus.InboundContext{
Channel: "slack",
ChatID: "C123456",
TopicID: "1234567890.123456",
})
if deliveryChatID != "C123456/1234567890.123456" {
t.Fatalf("deliveryChatID = %q, want %q", deliveryChatID, "C123456/1234567890.123456")
}
if channelID != "C123456" {
t.Fatalf("channelID = %q, want %q", channelID, "C123456")
}
if threadTS != "1234567890.123456" {
t.Fatalf("threadTS = %q, want %q", threadTS, "1234567890.123456")
}
}
func TestStripBotMention(t *testing.T) {
ch := &SlackChannel{botUserID: "U12345BOT"}
tests := []struct {
name string
input string
want string
}{
{
name: "mention at start",
input: "<@U12345BOT> hello there",
want: "hello there",
},
{
name: "mention in middle",
input: "hey <@U12345BOT> can you help",
want: "hey can you help",
},
{
name: "no mention",
input: "hello world",
want: "hello world",
},
{
name: "empty string",
input: "",
want: "",
},
{
name: "only mention",
input: "<@U12345BOT>",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ch.stripBotMention(tt.input)
if got != tt.want {
t.Errorf("stripBotMention(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestNewSlackChannel(t *testing.T) {
msgBus := bus.NewMessageBus()
bc := &config.Channel{Type: "slack", Enabled: true}
t.Run("missing bot token", func(t *testing.T) {
cfg := &config.SlackSettings{}
cfg.AppToken = *config.NewSecureString("xapp-test")
_, err := NewSlackChannel(bc, cfg, msgBus)
if err == nil {
t.Error("expected error for missing bot_token, got nil")
}
})
t.Run("missing app token", func(t *testing.T) {
cfg := &config.SlackSettings{}
cfg.BotToken = *config.NewSecureString("xoxb-test")
_, err := NewSlackChannel(bc, cfg, msgBus)
if err == nil {
t.Error("expected error for missing app_token, got nil")
}
})
t.Run("valid config", func(t *testing.T) {
cfg := &config.SlackSettings{}
cfg.BotToken = *config.NewSecureString("xoxb-test")
cfg.AppToken = *config.NewSecureString("xapp-test")
bc := &config.Channel{Type: "slack", Enabled: true, AllowFrom: []string{"U123"}}
ch, err := NewSlackChannel(bc, cfg, msgBus)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ch.Name() != "slack" {
t.Errorf("Name() = %q, want %q", ch.Name(), "slack")
}
if ch.IsRunning() {
t.Error("new channel should not be running")
}
})
}
func TestSlackChannelIsAllowed(t *testing.T) {
msgBus := bus.NewMessageBus()
t.Run("empty allowlist allows all", func(t *testing.T) {
bc := &config.Channel{Type: config.ChannelSlack, Enabled: true, AllowFrom: []string{}}
cfg := &config.SlackSettings{}
cfg.BotToken = *config.NewSecureString("xoxb-test")
cfg.AppToken = *config.NewSecureString("xapp-test")
ch, _ := NewSlackChannel(bc, cfg, msgBus)
if !ch.IsAllowed("U_ANYONE") {
t.Error("empty allowlist should allow all users")
}
})
t.Run("allowlist restricts users", func(t *testing.T) {
bc := &config.Channel{Type: config.ChannelSlack, Enabled: true, AllowFrom: []string{"U_ALLOWED"}}
cfg := &config.SlackSettings{}
cfg.BotToken = *config.NewSecureString("xoxb-test")
cfg.AppToken = *config.NewSecureString("xapp-test")
ch, _ := NewSlackChannel(bc, cfg, msgBus)
if !ch.IsAllowed("U_ALLOWED") {
t.Error("allowed user should pass allowlist check")
}
if ch.IsAllowed("U_BLOCKED") {
t.Error("non-allowed user should be blocked")
}
})
}
func TestSendMedia_SendsCaptionFallbackAfterUploads(t *testing.T) {
ch := &SlackChannel{
BaseChannel: channels.NewBaseChannel("slack", nil, nil, nil),
}
ch.SetRunning(true)
store := media.NewFileMediaStore()
ch.SetMediaStore(store)
tmpDir := t.TempDir()
localPath := filepath.Join(tmpDir, "report.txt")
if err := os.WriteFile(localPath, []byte("attachment body"), 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
ref, err := store.Store(localPath, media.MediaMeta{
Filename: "report.txt",
ContentType: "text/plain",
}, "test-scope")
if err != nil {
t.Fatalf("Store() error = %v", err)
}
var uploaded []slackUploadRecord
var posted []string
ch.uploadFileFn = func(ctx context.Context, params slacksdk.UploadFileParameters) error {
uploaded = append(uploaded, slackUploadRecord{
Channel: params.Channel,
Thread: params.ThreadTimestamp,
File: params.File,
Name: params.Filename,
Title: params.Title,
})
return nil
}
ch.postTextFn = func(ctx context.Context, channelID, threadTS, text string) error {
posted = append(posted, channelID+"|"+threadTS+"|"+text)
return nil
}
_, err = ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
ChatID: "C123456/1234567890.123456",
Parts: []bus.MediaPart{{
Ref: ref,
Type: "file",
Filename: "report.txt",
ContentType: "text/plain",
Caption: "shared caption",
}},
})
if err != nil {
t.Fatalf("SendMedia() error = %v", err)
}
if len(uploaded) != 1 {
t.Fatalf("uploads = %v, want 1 upload", uploaded)
}
if uploaded[0].Title != "shared caption" {
t.Fatalf("upload title = %q, want shared caption", uploaded[0].Title)
}
if len(posted) != 1 || posted[0] != "C123456|1234567890.123456|shared caption" {
t.Fatalf("posted = %v, want fallback text in same thread", posted)
}
}
type slackUploadRecord struct {
Channel string
Thread string
File string
Name string
Title string
}