Files
gronod 7bcf5322e1 feat: emby-mcp 1.0.16 player owner filter and HA links
Expose session/last-used user and device IP on player rows. Filter
retrieve_player_list by users, merge GET /Devices when include_offline
is set, and resolve spoken names to HA media_player entities via
player_links (device_id or IP). PlayNow still requires a live session.
2026-09-25 19:58:58 +00:00

156 lines
3.8 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
"time"
)
// clearEnv empties every config variable so a developer/CI environment cannot
// leak into Load results (empty variables are treated as unset by Load).
func clearEnv(t *testing.T) {
t.Helper()
for _, k := range []string{
"EMBY_SERVER_URL", "EMBY_USERNAME", "EMBY_PASSWORD",
"EMBY_API_KEY", "EMBY_USER_ID", "EMBY_VERIFY_SSL", "LLM_MAX_ITEMS",
"MCP_TRANSPORT", "MCP_LISTEN_ADDR", "MCP_SESSION_TIMEOUT",
"PLAYER_USERS", "PLAYER_LINKS",
} {
t.Setenv(k, "")
}
}
func writeEnv(t *testing.T, content string) string {
t.Helper()
p := filepath.Join(t.TempDir(), ".env")
if err := os.WriteFile(p, []byte(content), 0600); err != nil {
t.Fatal(err)
}
return p
}
func TestLoadBasic(t *testing.T) {
clearEnv(t)
p := writeEnv(t, `
# comment
EMBY_SERVER_URL = "http://emby.local:8096/"
EMBY_USERNAME = 'user'
EMBY_PASSWORD = pass
EMBY_VERIFY_SSL = False
LLM_MAX_ITEMS = 42
`)
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
if cfg.ServerURL != "http://emby.local:8096" {
t.Errorf("ServerURL = %q", cfg.ServerURL)
}
if cfg.Username != "user" || cfg.Password != "pass" {
t.Errorf("creds = %q/%q", cfg.Username, cfg.Password)
}
if cfg.VerifySSL {
t.Error("VerifySSL should be false")
}
if cfg.MaxChunkSize != 42 {
t.Errorf("MaxChunkSize = %d", cfg.MaxChunkSize)
}
}
func TestLoadAPIKeyRequiresUserID(t *testing.T) {
clearEnv(t)
p := writeEnv(t, `EMBY_SERVER_URL="http://x"
EMBY_API_KEY="abc"`)
if _, err := Load(p); err == nil {
t.Fatal("expected error for missing EMBY_USER_ID")
}
p2 := writeEnv(t, `EMBY_SERVER_URL="http://x"
EMBY_API_KEY="abc"
EMBY_USER_ID="u1"`)
cfg, err := Load(p2)
if err != nil {
t.Fatal(err)
}
if cfg.APIKey != "abc" || cfg.UserID != "u1" {
t.Errorf("cfg = %+v", cfg)
}
}
func TestLoadMissingFile(t *testing.T) {
clearEnv(t)
if _, err := Load(filepath.Join(t.TempDir(), "nope.env")); err == nil {
t.Fatal("expected error for missing file")
}
}
func TestLoadDefaults(t *testing.T) {
clearEnv(t)
p := writeEnv(t, `EMBY_SERVER_URL="http://x"
EMBY_USERNAME="u"
EMBY_PASSWORD="p"`)
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
if !cfg.VerifySSL {
t.Error("VerifySSL should default to true")
}
if cfg.Transport != TransportStdio {
t.Errorf("Transport = %q", cfg.Transport)
}
if cfg.ListenAddr != "0.0.0.0:8085" {
t.Errorf("ListenAddr = %q", cfg.ListenAddr)
}
if cfg.SessionTimeout != 30*time.Minute {
t.Errorf("SessionTimeout = %v", cfg.SessionTimeout)
}
}
func TestLoadHTTPMode(t *testing.T) {
clearEnv(t)
// HTTP mode authenticates per request; no server-side creds needed.
p := writeEnv(t, `EMBY_SERVER_URL="http://x"
MCP_TRANSPORT=http
MCP_LISTEN_ADDR="0.0.0.0:9090"
MCP_SESSION_TIMEOUT="5m"`)
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
if cfg.Transport != TransportHTTP || cfg.ListenAddr != "0.0.0.0:9090" || cfg.SessionTimeout != 5*time.Minute {
t.Errorf("cfg = %+v", cfg)
}
}
func TestLoadInvalidTransport(t *testing.T) {
clearEnv(t)
p := writeEnv(t, `EMBY_SERVER_URL="http://x"
MCP_TRANSPORT=grpc`)
if _, err := Load(p); err == nil {
t.Fatal("expected error for invalid transport")
}
}
func TestLoadPlayerPolicy(t *testing.T) {
clearEnv(t)
p := writeEnv(t, `EMBY_SERVER_URL="http://x"
EMBY_USERNAME=u
EMBY_PASSWORD=p
PLAYER_USERS=Gordon, Alice
PLAYER_LINKS=[{"names":"lounge,living room","ha_entity":"media_player.lounge","device_ip":"192.168.0.20","ha_source":"Emby"}]`)
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
if len(cfg.PlayerUsers) != 2 || cfg.PlayerUsers[0] != "Gordon" {
t.Fatalf("users = %#v", cfg.PlayerUsers)
}
if len(cfg.PlayerLinks) != 1 || cfg.PlayerLinks[0].HAEntity != "media_player.lounge" {
t.Fatalf("links = %#v", cfg.PlayerLinks)
}
if len(cfg.PlayerLinks[0].Names) != 2 {
t.Fatalf("names = %#v", cfg.PlayerLinks[0].Names)
}
}