248 lines
7.1 KiB
Go
248 lines
7.1 KiB
Go
package discord
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
"strconv"
|
|
|
|
"plumber/internal/events"
|
|
"plumber/internal/store"
|
|
)
|
|
|
|
type recordedSend struct {
|
|
Kind string
|
|
ChannelID string
|
|
Name string
|
|
Msg Message
|
|
}
|
|
|
|
type fakeAPI struct {
|
|
mu sync.Mutex
|
|
sends []recordedSend
|
|
edits []recordedSend
|
|
next int
|
|
failSend error
|
|
}
|
|
|
|
func (f *fakeAPI) SendToChannel(_ context.Context, channelID string, msg Message) (string, error) {
|
|
return f.record("channel", channelID, "", msg)
|
|
}
|
|
|
|
func (f *fakeAPI) StartThread(_ context.Context, channelID, messageID, name string) (string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.next++
|
|
f.sends = append(f.sends, recordedSend{
|
|
Kind: "thread",
|
|
ChannelID: channelID,
|
|
Name: name,
|
|
Msg: Message{ThreadName: name, URL: messageID},
|
|
})
|
|
return "thread-" + messageID, nil
|
|
}
|
|
|
|
func (f *fakeAPI) SendToThread(_ context.Context, threadID string, msg Message) (string, error) {
|
|
return f.record("thread-msg", threadID, "", msg)
|
|
}
|
|
|
|
func (f *fakeAPI) Edit(_ context.Context, channelID, messageID string, msg Message) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.edits = append(f.edits, recordedSend{
|
|
Kind: "edit",
|
|
ChannelID: channelID,
|
|
Name: messageID,
|
|
Msg: msg,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeAPI) Close() error { return nil }
|
|
|
|
func (f *fakeAPI) record(kind, channelID, name string, msg Message) (string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.failSend != nil {
|
|
return "", f.failSend
|
|
}
|
|
f.next++
|
|
id := "msg-" + strconv.Itoa(f.next)
|
|
f.sends = append(f.sends, recordedSend{Kind: kind, ChannelID: channelID, Name: name, Msg: msg})
|
|
return id, nil
|
|
}
|
|
|
|
func TestOutboundRootReplyAndEdit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
links := newMemoryLinks()
|
|
api := &fakeAPI{}
|
|
bot := New("channel-1", links, api)
|
|
ctx := context.Background()
|
|
|
|
root := events.PostEvent{
|
|
PostID: "root-1",
|
|
RootID: "root-1",
|
|
Title: "Leaky sink",
|
|
Body: "Water under the cabinet.",
|
|
City: "Oakland",
|
|
AuthorName: "sam",
|
|
Permalink: "https://www.askaplumberfirst.com/questions/root-1#post-root-1",
|
|
Images: []events.Image{{URL: "https://cdn.example/a.jpg"}, {URL: "https://cdn.example/b.jpg"}},
|
|
}
|
|
bot.Handle(ctx, events.PostCreated{PostEvent: root})
|
|
|
|
if len(api.sends) != 2 || api.sends[0].Kind != "channel" || api.sends[1].Kind != "thread" {
|
|
t.Fatalf("root sends = %+v", api.sends)
|
|
}
|
|
if api.sends[0].ChannelID != "channel-1" || api.sends[1].Name != "Leaky sink" {
|
|
t.Fatalf("root routing = %+v", api.sends)
|
|
}
|
|
if got := api.sends[0].Msg.ImageURLs; len(got) != 2 || got[0] != "https://cdn.example/a.jpg" {
|
|
t.Fatalf("root images = %v", got)
|
|
}
|
|
link, err := links.GetByPostID(ctx, "root-1")
|
|
if err != nil || link.MessageID != "msg-1" || link.ThreadID != "thread-msg-1" {
|
|
t.Fatalf("root link = %+v, %v", link, err)
|
|
}
|
|
|
|
reply := events.PostEvent{
|
|
PostID: "reply-1",
|
|
RootID: "root-1",
|
|
ParentID: "root-1",
|
|
Body: "Replace the cartridge.",
|
|
AuthorName: "plumber",
|
|
Permalink: "https://www.askaplumberfirst.com/questions/root-1#post-reply-1",
|
|
}
|
|
bot.Handle(ctx, events.PostCreated{PostEvent: reply})
|
|
if len(api.sends) != 3 || api.sends[2].Kind != "thread-msg" || api.sends[2].ChannelID != "thread-msg-1" {
|
|
t.Fatalf("reply sends = %+v", api.sends)
|
|
}
|
|
replyLink, err := links.GetByPostID(ctx, "reply-1")
|
|
if err != nil || replyLink.MessageID != "msg-3" || replyLink.ThreadID != "" {
|
|
t.Fatalf("reply link = %+v, %v", replyLink, err)
|
|
}
|
|
|
|
root.Body = "Updated leak."
|
|
bot.Handle(ctx, events.PostUpdated{PostEvent: root})
|
|
if len(api.edits) != 1 || api.edits[0].ChannelID != "channel-1" || api.edits[0].Name != "msg-1" {
|
|
t.Fatalf("root edit = %+v", api.edits)
|
|
}
|
|
if api.edits[0].Msg.Description != "Updated leak." {
|
|
t.Fatalf("root edit body = %+v", api.edits[0].Msg)
|
|
}
|
|
|
|
reply.Body = "Use a ceramic cartridge."
|
|
bot.Handle(ctx, events.PostUpdated{PostEvent: reply})
|
|
if len(api.edits) != 2 || api.edits[1].ChannelID != "thread-msg-1" || api.edits[1].Name != "msg-3" {
|
|
t.Fatalf("reply edit = %+v", api.edits)
|
|
}
|
|
}
|
|
|
|
func TestOutboundSkipsReplyWithoutRootLink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
api := &fakeAPI{}
|
|
bot := New("channel-1", newMemoryLinks(), api)
|
|
bot.Handle(context.Background(), events.PostCreated{PostEvent: events.PostEvent{
|
|
PostID: "reply-1",
|
|
RootID: "missing",
|
|
ParentID: "missing",
|
|
Body: "Orphan reply",
|
|
}})
|
|
if len(api.sends) != 0 {
|
|
t.Fatalf("unexpected sends %+v", api.sends)
|
|
}
|
|
}
|
|
|
|
func TestOutboundUpdateWithoutLinkCreates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
links := newMemoryLinks()
|
|
api := &fakeAPI{}
|
|
bot := New("channel-1", links, api)
|
|
bot.Handle(context.Background(), events.PostUpdated{PostEvent: events.PostEvent{
|
|
PostID: "root-2",
|
|
RootID: "root-2",
|
|
Title: "Late question",
|
|
Body: "Created while Discord was down.",
|
|
}})
|
|
link, err := links.GetByPostID(context.Background(), "root-2")
|
|
if err != nil || link.ThreadID == "" || len(api.sends) != 2 {
|
|
t.Fatalf("late create link=%+v sends=%+v err=%v", link, api.sends, err)
|
|
}
|
|
}
|
|
|
|
func TestFormatMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := formatMessage(events.PostEvent{
|
|
Title: "Leaky sink",
|
|
Body: "It drips.",
|
|
City: "Oakland",
|
|
AuthorName: "sam",
|
|
Permalink: "https://example.com/q",
|
|
Images: []events.Image{{URL: "https://cdn.example/a.jpg", Description: "ignored"}},
|
|
})
|
|
if got.Title != "Leaky sink" ||
|
|
got.Description != "It drips." ||
|
|
got.City != "Oakland" ||
|
|
got.Author != "sam" ||
|
|
got.URL != "https://example.com/q" ||
|
|
got.ThreadName != "Leaky sink" ||
|
|
len(got.ImageURLs) != 1 {
|
|
t.Fatalf("format = %+v", got)
|
|
}
|
|
|
|
reply := formatMessage(events.PostEvent{Body: "Thanks", AuthorName: ""})
|
|
if reply.Title != "Reply" || reply.Author != "Someone" || reply.ThreadName != "Question" {
|
|
t.Fatalf("reply format = %+v", reply)
|
|
}
|
|
}
|
|
|
|
func TestFromEnvDisabled(t *testing.T) {
|
|
t.Setenv("DISCORD_BOT_TOKEN", "")
|
|
t.Setenv("DISCORD_CHANNEL_ID", "")
|
|
bot, err := FromEnv(newMemoryLinks(), nil, nil, nil)
|
|
if err != nil || bot != nil {
|
|
t.Fatalf("disabled FromEnv = (%v, %v)", bot, err)
|
|
}
|
|
}
|
|
|
|
func TestFromEnvRequiresBoth(t *testing.T) {
|
|
t.Setenv("DISCORD_BOT_TOKEN", "token")
|
|
t.Setenv("DISCORD_CHANNEL_ID", "")
|
|
if _, err := FromEnv(newMemoryLinks(), nil, nil, nil); err == nil {
|
|
t.Fatal("expected error when channel is missing")
|
|
}
|
|
t.Setenv("DISCORD_BOT_TOKEN", "")
|
|
t.Setenv("DISCORD_CHANNEL_ID", "channel")
|
|
if _, err := FromEnv(newMemoryLinks(), nil, nil, nil); err == nil {
|
|
t.Fatal("expected error when token is missing")
|
|
}
|
|
}
|
|
|
|
func TestMemoryLinkUpsertKeepsThread(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
links := newMemoryLinks()
|
|
ctx := context.Background()
|
|
if err := links.Upsert(ctx, store.DiscordLink{PostID: "p", MessageID: "m1", ThreadID: "t1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := links.Upsert(ctx, store.DiscordLink{PostID: "p", MessageID: "m2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := links.GetByPostID(ctx, "p")
|
|
if err != nil || got.MessageID != "m2" || got.ThreadID != "t1" {
|
|
t.Fatalf("upsert keep thread = %+v, %v", got, err)
|
|
}
|
|
if _, err := links.GetByMessageID(ctx, "m2"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := links.GetRootByThreadID(ctx, "t1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|