mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
265 lines
6.8 KiB
Go
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
|
|
}
|