package mediaserver import ( "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestParsePlexSectionsFull(t *testing.T) { body := []byte(`{ "MediaContainer": { "Directory": [ { "key": "1", "title": "Movies", "Location": [{"path": "/data/media/movies"}] }, { "key": "2", "title": "TV Shows", "Location": [{"path": "/data/media/tv"}, {"path": "/mnt/tv2"}] }, { "key": "0", "title": "Bogus", "Location": [{"path": "/skip"}] } ] } }`) sections, err := parsePlexSectionsFull(body) if err != nil { t.Fatalf("parsePlexSectionsFull error: %v", err) } if len(sections) != 2 { t.Fatalf("got %d sections, want 2 (id=0 should be skipped)", len(sections)) } if sections[0].ID != 1 || sections[0].Title != "Movies" { t.Errorf("section[0] = %+v", sections[0]) } if sections[1].ID != 2 || len(sections[1].Locations) != 2 { t.Errorf("section[1] = %+v", sections[1]) } } func TestMatchSectionByPath(t *testing.T) { sections := []Section{ {ID: 1, Title: "Movies", Locations: []string{"/data/media/movies"}}, {ID: 2, Title: "TV", Locations: []string{"/data/media/tv"}}, {ID: 3, Title: "TV-HD", Locations: []string{"/data/media/tv/hd"}}, } tests := []struct { path string wantID int wantOK bool }{ {"/data/media/movies/Inception (2010)/Inception.mkv", 1, true}, {"/data/media/tv/Show/Season 01/ep.mkv", 2, true}, {"/data/media/tv/hd/Show/Season 01/ep.mkv", 3, true}, // most specific wins {"/data/media/movies", 1, true}, // exact {"/elsewhere/foo.mkv", 0, false}, } for _, tc := range tests { gotID, gotOK := matchSectionByPath(sections, tc.path) if gotID != tc.wantID || gotOK != tc.wantOK { t.Errorf("matchSectionByPath(%q) = (%d,%v), want (%d,%v)", tc.path, gotID, gotOK, tc.wantID, tc.wantOK) } } } func TestRefreshPlex_PartialRefreshWithPath(t *testing.T) { var refreshHits int32 var gotPath, gotToken string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/library/sections": w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "MediaContainer": { "Directory": [ { "key": "7", "title": "Movies", "Location": [{"path": "/m"}] } ] } }`)) case strings.HasPrefix(r.URL.Path, "/library/sections/7/refresh"): atomic.AddInt32(&refreshHits, 1) gotPath = r.URL.Query().Get("path") gotToken = r.URL.Query().Get("X-Plex-Token") w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusNotFound) } })) defer srv.Close() // Reset cache so the test server is hit fresh. plexSectionMu.Lock() plexSectionCache = map[string][]Section{} plexSectionMu.Unlock() cfg := ServerConfig{Kind: "plex", URL: srv.URL, Token: "tk-1"} Refresh([]ServerConfig{cfg}, "/m/Inception/Inception.mkv") // Refresh fans out goroutines — give it time. deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if atomic.LoadInt32(&refreshHits) > 0 { break } time.Sleep(20 * time.Millisecond) } if atomic.LoadInt32(&refreshHits) != 1 { t.Fatalf("refresh endpoint hit %d times, want 1", refreshHits) } if gotPath != "/m/Inception/Inception.mkv" { t.Errorf("path query = %q", gotPath) } if gotToken != "tk-1" { t.Errorf("token query = %q", gotToken) } } func TestRefreshJellyfin(t *testing.T) { var hits int32 var gotToken string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost && r.URL.Path == "/Library/Refresh" { atomic.AddInt32(&hits, 1) gotToken = r.Header.Get("X-Emby-Token") w.WriteHeader(http.StatusNoContent) return } w.WriteHeader(http.StatusNotFound) })) defer srv.Close() cfg := ServerConfig{Kind: "jellyfin", URL: srv.URL, Token: "jf-key"} Refresh([]ServerConfig{cfg}, "") deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if atomic.LoadInt32(&hits) > 0 { break } time.Sleep(20 * time.Millisecond) } if atomic.LoadInt32(&hits) != 1 { t.Fatalf("Jellyfin hits = %d, want 1", hits) } if gotToken != "jf-key" { t.Errorf("X-Emby-Token = %q", gotToken) } }