Files
goclaw/internal/channels/slack/stream_test.go
T
Duc Nguyen 137a986d4f feat(channels): add Slack channel (#83)
* feat(channels): add Slack channel via Socket Mode (#37)

Implement Slack integration using Socket Mode (xapp-/xoxb- tokens):
- Event-driven messaging via app_mention + message events
- Policy checks: open, pairing, allowlist, disabled (DM + group)
- Thread participation with configurable TTL
- Markdown-to-mrkdwn formatting pipeline
- Streaming support (edit-in-place + native ChatStreamer)
- SSRF-protected file downloads
- Debounce, dedup, reactions, group history context
- 170 unit tests (format, helpers, stream, SSRF)

Fix BaseChannel.HandleMessage allowlist to also check chatID,
enabling group allowlist with channel IDs across all channels.

Closes #37

* feat(slack): add file/media support and edit-to-mention handling

- Wire inbound file download into handleMessage (images, audio, documents)
- Add media.go with resolveMedia, classifyMime, buildMediaTags
- Extract shared ExtractDocumentContent to channels/media_utils.go (DRY with Telegram)
- Support file_share and message_changed subtypes
- Handle edit-to-mention: respond when user edits old message to add @bot
- Add MediaMaxBytes config field (default 20MB)
- Fix debounce media accumulation (was silently dropping files)
- Add 60s HTTP client timeout on file downloads
- Refactor downloadFile signature for slack.File compatibility
2026-03-09 07:02:37 +07:00

122 lines
2.3 KiB
Go

package slack
import (
"testing"
)
func TestExtractChannelID(t *testing.T) {
tests := []struct {
name string
localKey string
expected string
}{
{
name: "plain channel id",
localKey: "C123456",
expected: "C123456",
},
{
name: "threaded message",
localKey: "C123456:thread:1234.5678",
expected: "C123456",
},
{
name: "threaded with different ts format",
localKey: "C999:thread:999999.999999",
expected: "C999",
},
{
name: "no thread marker",
localKey: "C123456789",
expected: "C123456789",
},
{
name: "empty string",
localKey: "",
expected: "",
},
{
name: "only thread marker",
localKey: ":thread:1234.5678",
expected: ":thread:1234.5678",
},
{
name: "colon but no thread text",
localKey: "C123:thread:",
expected: "C123",
},
{
name: "multiple colons",
localKey: "C123:thread:1234:5678",
expected: "C123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractChannelID(tt.localKey)
if got != tt.expected {
t.Errorf("extractChannelID(%q) = %q, want %q", tt.localKey, got, tt.expected)
}
})
}
}
func TestExtractThreadTS(t *testing.T) {
tests := []struct {
name string
localKey string
expected string
}{
{
name: "threaded message",
localKey: "C123456:thread:1234.5678",
expected: "1234.5678",
},
{
name: "plain channel id",
localKey: "C123456",
expected: "",
},
{
name: "empty string",
localKey: "",
expected: "",
},
{
name: "only thread marker (no channel id)",
localKey: ":thread:1234.5678",
expected: "", // idx must be > 0, so this returns ""
},
{
name: "thread marker but empty ts",
localKey: "C123:thread:",
expected: "",
},
{
name: "multiple colons after thread",
localKey: "C123:thread:1234:5678:extra",
expected: "1234:5678:extra",
},
{
name: "thread marker not found",
localKey: "C123:other:1234.5678",
expected: "",
},
{
name: "thread marker case sensitive",
localKey: "C123:THREAD:1234.5678",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractThreadTS(tt.localKey)
if got != tt.expected {
t.Errorf("extractThreadTS(%q) = %q, want %q", tt.localKey, got, tt.expected)
}
})
}
}