Files
gronod 2d5763d6c6 feat: slim tool responses, drop lyrics search, rename module (1.0.11)
Item-shaped results now omit empty fields and drop low-value metadata
(overview, lyrics, file_path, creation_date, premiere_date, bitrate);
absent numeric fields drop the key instead of emitting "". Player
sessions report hh:mm:ss times only; playlists drop overview/date_created.
The lyrics_or_description search parameter is removed, along with the
unused DTO fields, and upstream Fields requests are slimmed to match.
Module path renamed to git.i3omb.com/gronod/emby-mcp.
2026-09-21 23:54:07 +01:00

126 lines
3.4 KiB
Go

package server
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.i3omb.com/gronod/emby-mcp/internal/emby"
"git.i3omb.com/gronod/emby-mcp/internal/state"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func mkItems(n int) []emby.MediaItem {
out := make([]emby.MediaItem, n)
for i := range out {
out[i] = emby.MediaItem{Title: "t", ItemID: string(rune('a' + i))}
}
return out
}
func TestNextChunkJSON(t *testing.T) {
st := state.New(nil, "u1", 2)
st.SetSearch(&state.SearchChunk{
SearchID: "sid", TotalItems: 5, ChunkSize: 2, Items: mkItems(5),
})
var r chunkResult
if err := json.Unmarshal([]byte(nextChunkJSON(st)), &r); err != nil {
t.Fatal(err)
}
if r.ChunkNumber != 1 || r.ChunkSize != 2 || !r.MoreChunksAvailable || len(r.Items) != 2 {
t.Fatalf("chunk1 = %+v", r)
}
json.Unmarshal([]byte(nextChunkJSON(st)), &r)
if r.ChunkNumber != 2 || !r.MoreChunksAvailable {
t.Fatalf("chunk2 = %+v", r)
}
json.Unmarshal([]byte(nextChunkJSON(st)), &r)
if r.ChunkNumber != 3 || r.ChunkSize != 1 || r.MoreChunksAvailable || len(r.Items) != 1 {
t.Fatalf("chunk3 = %+v", r)
}
// State cleared after last chunk.
if got := nextChunkJSON(st); got != "{}" {
t.Fatalf("after last chunk = %s", got)
}
}
func TestNextChunkJSONEmpty(t *testing.T) {
st := state.New(nil, "u1", 2)
if got := nextChunkJSON(st); got != "{}" {
t.Fatalf("empty = %s", got)
}
}
// end-to-end: MCP client calling tools over in-memory transport against a
// mocked Emby server.
func TestToolEndToEnd(t *testing.T) {
embySrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/Library/MediaFolders":
json.NewEncoder(w).Encode(map[string]any{
"TotalRecordCount": 1,
"Items": []map[string]any{
{"Name": "Music", "Type": "CollectionFolder", "CollectionType": "music", "Id": "L1"},
},
})
default:
t.Errorf("unexpected call %s %s", r.Method, r.URL.Path)
}
}))
defer embySrv.Close()
st := state.New(emby.NewClient(embySrv.URL, emby.Options{VerifySSL: true}), "u1", 10)
srv := New(st)
ctx := context.Background()
t1, t2 := mcp.NewInMemoryTransports()
if _, err := srv.Connect(ctx, t1, nil); err != nil {
t.Fatal(err)
}
client := mcp.NewClient(&mcp.Implementation{Name: "test"}, nil)
cs, err := client.Connect(ctx, t2, nil)
if err != nil {
t.Fatal(err)
}
defer cs.Close()
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "retrieve_library_list"})
if err != nil {
t.Fatal(err)
}
var libs []emby.Library
txt := res.Content[0].(*mcp.TextContent).Text
if err := json.Unmarshal([]byte(txt), &libs); err != nil {
t.Fatalf("result text = %q", txt)
}
if len(libs) != 1 || libs[0].Name != "Music" {
t.Fatalf("libs = %+v", libs)
}
// select_library by name (case-insensitive).
res, err = cs.CallTool(ctx, &mcp.CallToolParams{
Name: "select_library",
Arguments: map[string]any{"library_name": "music"},
})
if err != nil {
t.Fatal(err)
}
if txt := res.Content[0].(*mcp.TextContent).Text; txt != "Success" {
t.Fatalf("select = %q", txt)
}
// retrieve_current_library returns the selection as JSON.
res, err = cs.CallTool(ctx, &mcp.CallToolParams{Name: "retrieve_current_library"})
if err != nil {
t.Fatal(err)
}
var cur emby.Library
json.Unmarshal([]byte(res.Content[0].(*mcp.TextContent).Text), &cur)
if cur.ID != "L1" {
t.Fatalf("current = %+v", cur)
}
}