package bot import ( "context" "errors" "strings" "sync" "testing" "bellhop/internal/arr" "maunium.net/go/mautrix/id" ) type stubClient struct { results []arr.Result addErr error srchErr error searched string added []arr.Result } func (s *stubClient) Search(_ context.Context, term string) ([]arr.Result, error) { s.searched = term if s.srchErr != nil { return nil, s.srchErr } return s.results, nil } func (s *stubClient) Add(_ context.Context, r arr.Result) error { if s.addErr != nil { return s.addErr } s.added = append(s.added, r) return nil } type stubReplier struct { mu sync.Mutex plain string htmlBody string calls int } func (r *stubReplier) PostThreadedReply(_ context.Context, _ id.RoomID, _ id.EventID, plain, htmlBody string) error { r.mu.Lock() defer r.mu.Unlock() r.calls++ r.plain = plain r.htmlBody = htmlBody return nil } func newDispatcher(prefix string, services Services, replier Replier) *Dispatcher { return New(context.Background(), prefix, services, replier) } const ( room = id.RoomID("!room:example.org") event = id.EventID("$evt") sender = id.UserID("@alice:example.org") ) func TestParseCommand(t *testing.T) { cases := []struct { body string wantCmd, wantQ string wantOk bool }{ {"!movie dune", "movie", "dune", true}, {" !movie dune part two ", "movie", "dune part two", true}, {"!HELP", "help", "", true}, {"hello world", "", "", false}, {"!", "", "", false}, {"! ", "", "", false}, } for _, c := range cases { cmd, q, ok := parseCommand("!", c.body) if cmd != c.wantCmd || q != c.wantQ || ok != c.wantOk { t.Errorf("parseCommand(%q) = (%q,%q,%v); want (%q,%q,%v)", c.body, cmd, q, ok, c.wantCmd, c.wantQ, c.wantOk) } } } func TestDispatchMovieTopHit(t *testing.T) { radarr := &stubClient{results: []arr.Result{ {Title: "Dune", Year: 2021}, {Title: "Dune", Year: 1984}, }} rep := &stubReplier{} d := newDispatcher("!", Services{Radarr: radarr}, rep) d.Handle(room, event, sender, "!movie dune") if radarr.searched != "dune" { t.Fatalf("search term = %q", radarr.searched) } if len(radarr.added) != 1 || radarr.added[0].Year != 2021 { t.Fatalf("expected top hit added, got %+v", radarr.added) } if !strings.Contains(rep.plain, "Dune (2021)") || !strings.Contains(rep.plain, "Radarr") { t.Fatalf("reply missing expected content: %q", rep.plain) } } func TestDispatchNoResults(t *testing.T) { sonarr := &stubClient{} rep := &stubReplier{} d := newDispatcher("!", Services{Sonarr: sonarr}, rep) d.Handle(room, event, sender, "!tv obscure show") if len(sonarr.added) != 0 { t.Fatal("should not add when no results") } if !strings.Contains(rep.plain, "No Sonarr results") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestDispatchUnconfigured(t *testing.T) { rep := &stubReplier{} d := newDispatcher("!", Services{}, rep) d.Handle(room, event, sender, "!music radiohead") if !strings.Contains(rep.plain, "Lidarr is not configured") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestDispatchEmptyQuery(t *testing.T) { radarr := &stubClient{} rep := &stubReplier{} d := newDispatcher("!", Services{Radarr: radarr}, rep) d.Handle(room, event, sender, "!movie") if radarr.searched != "" { t.Fatal("should not search with empty query") } if !strings.Contains(rep.plain, "Usage:") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestDispatchAddError(t *testing.T) { radarr := &stubClient{ results: []arr.Result{{Title: "Dune", Year: 2021}}, addErr: errors.New("boom"), } rep := &stubReplier{} d := newDispatcher("!", Services{Radarr: radarr}, rep) d.Handle(room, event, sender, "!movie dune") if !strings.Contains(rep.plain, "failed to add") || !strings.Contains(rep.plain, "boom") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestDispatchSearchError(t *testing.T) { radarr := &stubClient{srchErr: errors.New("network down")} rep := &stubReplier{} d := newDispatcher("!", Services{Radarr: radarr}, rep) d.Handle(room, event, sender, "!movie dune") if !strings.Contains(rep.plain, "search failed") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestDispatchHelp(t *testing.T) { rep := &stubReplier{} d := newDispatcher("!", Services{}, rep) d.Handle(room, event, sender, "!help") for _, want := range []string{"!movie", "!tv", "!music", "!help"} { if !strings.Contains(rep.plain, want) { t.Errorf("help missing %q: %q", want, rep.plain) } } } func TestDispatchUnknown(t *testing.T) { rep := &stubReplier{} d := newDispatcher("!", Services{}, rep) d.Handle(room, event, sender, "!frobnicate stuff") if !strings.Contains(rep.plain, "Unknown command") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestNonCommandIgnored(t *testing.T) { rep := &stubReplier{} d := newDispatcher("!", Services{}, rep) d.Handle(room, event, sender, "just chatting") if rep.calls != 0 { t.Fatalf("expected no reply, got %d", rep.calls) } } func TestDispatchAlreadyInLibrary(t *testing.T) { radarr := &stubClient{results: []arr.Result{{Title: "Dune", Year: 2021, Exists: true}}} rep := &stubReplier{} d := newDispatcher("!", Services{Radarr: radarr}, rep) d.Handle(room, event, sender, "!movie dune") if len(radarr.added) != 0 { t.Fatalf("should not re-add existing item, got %d adds", len(radarr.added)) } if !strings.Contains(rep.plain, "already has") || !strings.Contains(rep.plain, "Dune (2021)") { t.Fatalf("unexpected reply: %q", rep.plain) } } func TestCustomPrefix(t *testing.T) { radarr := &stubClient{results: []arr.Result{{Title: "Dune", Year: 2021}}} rep := &stubReplier{} d := newDispatcher(".bh ", Services{Radarr: radarr}, rep) d.Handle(room, event, sender, ".bh movie dune") if len(radarr.added) != 1 { t.Fatalf("expected add, got %d", len(radarr.added)) } }