package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "testing" "time" handler "github.com/tus/tusd/pkg/handler" "scm.bstein.dev/bstein/Pegasus/backend/internal" ) type fakeJellyfin struct { mu sync.Mutex authResult internal.AuthResult authErr error refreshTokens []string } func (f *fakeJellyfin) AuthenticateByName(username, password string) (internal.AuthResult, error) { return f.authResult, f.authErr } func (f *fakeJellyfin) RefreshLibrary(userToken string) { f.mu.Lock() defer f.mu.Unlock() f.refreshTokens = append(f.refreshTokens, userToken) } func (f *fakeJellyfin) refreshCount() int { f.mu.Lock() defer f.mu.Unlock() return len(f.refreshTokens) } func testUserMap(t *testing.T, payload string) *internal.UserMap { t.Helper() path := filepath.Join(t.TempDir(), "user-map.yaml") if err := os.WriteFile(path, []byte(payload), 0o644); err != nil { t.Fatalf("write user map: %v", err) } um, err := internal.LoadUserMap(path) if err != nil { t.Fatalf("load user map: %v", err) } return um } func sessionCookie(t *testing.T, username, token string) string { t.Helper() rr := httptest.NewRecorder() if err := internal.SetSession(rr, username, token); err != nil { t.Fatalf("SetSession: %v", err) } cookies := rr.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected session cookie") } return cookies[0].Name + "=" + cookies[0].Value } func requestWithCookie(method, target, cookie string, body []byte) *http.Request { req := httptest.NewRequest(method, target, bytes.NewReader(body)) if cookie != "" { req.Header.Set("Cookie", cookie) } return req } func TestRouterLifecycleAndFilesystemRoutes(t *testing.T) { origMediaRoot := mediaRoot origTusDir := tusDir origDebug := internal.Debug origDryRun := internal.DryRun defer func() { mediaRoot = origMediaRoot tusDir = origTusDir internal.Debug = origDebug internal.DryRun = origDryRun }() root := t.TempDir() mediaRoot = filepath.Join(root, "media") tusDir = filepath.Join(root, "tus") internal.Debug = false internal.DryRun = false if err := os.MkdirAll(filepath.Join(mediaRoot, "library", "album"), 0o2775); err != nil { t.Fatalf("mkdir media root: %v", err) } if err := os.WriteFile(filepath.Join(mediaRoot, "library", "old.txt"), []byte("old"), 0o644); err != nil { t.Fatalf("write source file: %v", err) } um := testUserMap(t, "map:\n brad: library\n") jf := &fakeJellyfin{ authResult: internal.AuthResult{ AccessToken: "jf-token", }, } jf.authResult.User.Name = "brad" router := buildRouter(um, jf) t.Run("health and version", func(t *testing.T) { rr := httptest.NewRecorder() router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/healthz", nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected health status %d", rr.Code) } rr = httptest.NewRecorder() router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/version", nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected version status %d", rr.Code) } }) t.Run("login whoami logout", func(t *testing.T) { loginBody, _ := json.Marshal(map[string]string{"username": "brad", "password": "pass"}) rr := httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/login", "", loginBody)) if rr.Code != http.StatusOK { t.Fatalf("unexpected login status %d: %s", rr.Code, rr.Body.String()) } cookies := rr.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected login cookie") } session := cookies[0].Name + "=" + cookies[0].Value rr = httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/api/whoami", session, nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected whoami status %d", rr.Code) } var profile map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &profile); err != nil { t.Fatalf("decode whoami: %v", err) } if profile["username"] != "brad" { t.Fatalf("unexpected whoami payload %#v", profile) } rr = httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/logout", session, nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected logout status %d", rr.Code) } }) session := sessionCookie(t, "brad", "jf-token") t.Run("list mkdir rename delete", func(t *testing.T) { rr := httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/api/list?lib=library", session, nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected list status %d: %s", rr.Code, rr.Body.String()) } mkdirBody, _ := json.Marshal(map[string]string{"lib": "library", "path": "new-folder"}) rr = httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/mkdir", session, mkdirBody)) if rr.Code != http.StatusOK { t.Fatalf("unexpected mkdir status %d: %s", rr.Code, rr.Body.String()) } if _, err := os.Stat(filepath.Join(mediaRoot, "library", "new-folder")); err != nil { t.Fatalf("expected new folder to exist: %v", err) } renameBody, _ := json.Marshal(map[string]string{ "lib": "library", "from": "old.txt", "to": "2026.01.02.Note.old.txt", }) rr = httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/rename", session, renameBody)) if rr.Code != http.StatusOK { t.Fatalf("unexpected rename status %d: %s", rr.Code, rr.Body.String()) } if _, err := os.Stat(filepath.Join(mediaRoot, "library", "2026.01.02.Note.old.txt")); err != nil { t.Fatalf("expected renamed file to exist: %v", err) } rr = httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodDelete, "/api/file?lib=library&path=new-folder&recursive=true", session, nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected delete status %d: %s", rr.Code, rr.Body.String()) } if _, err := os.Stat(filepath.Join(mediaRoot, "library", "new-folder")); !os.IsNotExist(err) { t.Fatalf("expected deleted folder to be absent, err=%v", err) } }) t.Run("static metrics and tus auth", func(t *testing.T) { rr := httptest.NewRecorder() router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected static status %d", rr.Code) } rr = httptest.NewRecorder() router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/metrics", nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected metrics status %d", rr.Code) } rr = httptest.NewRecorder() router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/tus/123", nil)) if rr.Code != http.StatusUnauthorized { t.Fatalf("expected tus auth to reject unauthenticated request, got %d", rr.Code) } }) if jf.refreshCount() == 0 { t.Fatalf("expected at least one Jellyfin refresh call") } } func TestDebugRoutesAndUploadProcessing(t *testing.T) { origMediaRoot := mediaRoot origTusDir := tusDir origDebug := internal.Debug origDryRun := internal.DryRun defer func() { mediaRoot = origMediaRoot tusDir = origTusDir internal.Debug = origDebug internal.DryRun = origDryRun }() root := t.TempDir() mediaRoot = filepath.Join(root, "media") tusDir = filepath.Join(root, "tus") internal.Debug = true internal.DryRun = false if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil { t.Fatalf("mkdir media root: %v", err) } um := testUserMap(t, "map:\n brad: library\n") jf := &fakeJellyfin{ authResult: internal.AuthResult{AccessToken: "jf-token"}, } jf.authResult.User.Name = "brad" router := buildRouter(um, jf) session := sessionCookie(t, "brad", "jf-token") rr := httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/debug/env", session, nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected debug env status %d", rr.Code) } rr = httptest.NewRecorder() router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/debug/write-test", session, nil)) if rr.Code != http.StatusOK { t.Fatalf("unexpected debug write-test status %d", rr.Code) } srcID := "upload-id-123" if err := os.MkdirAll(tusDir, 0o2775); err != nil { t.Fatalf("mkdir tus dir: %v", err) } if err := os.WriteFile(filepath.Join(tusDir, srcID+".bin"), []byte("payload"), 0o644); err != nil { t.Fatalf("write tus payload: %v", err) } event := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{ Header: http.Header{"Cookie": []string{session}}, }, Upload: handler.FileInfo{ ID: srcID, MetaData: map[string]string{ "filename": "video.mp4", "desc": "Trip", "date": "2026-01-02", "lib": "library", }, }, } done := make(chan struct{}) ch := make(chan handler.HookEvent, 2) go func() { watchUploadCompletions(ch, um, jf) close(done) }() ch <- event ch <- handler.HookEvent{ HTTPRequest: handler.HTTPRequest{ Header: http.Header{"Cookie": []string{session}}, }, Upload: handler.FileInfo{ ID: "bad-id", MetaData: map[string]string{ "filename": "clip.mp4", "lib": "library", }, }, } close(ch) select { case <-done: case <-time.After(2 * time.Second): t.Fatalf("watchUploadCompletions did not drain channel") } expected := filepath.Join(mediaRoot, "library", composeFinalName("2026-01-02", "Trip", "video.mp4")) if _, err := os.Stat(expected); err != nil { t.Fatalf("expected uploaded file at destination: %v", err) } if jf.refreshCount() == 0 { t.Fatalf("expected upload completion to refresh Jellyfin") } } func TestProcessCompletedUploadErrorPaths(t *testing.T) { origMediaRoot := mediaRoot origTusDir := tusDir origDryRun := internal.DryRun defer func() { mediaRoot = origMediaRoot tusDir = origTusDir internal.DryRun = origDryRun }() root := t.TempDir() mediaRoot = filepath.Join(root, "media") tusDir = filepath.Join(root, "tus") internal.DryRun = false um := testUserMap(t, "map:\n brad: library\n") jf := &fakeJellyfin{authResult: internal.AuthResult{AccessToken: "jf-token"}} jf.authResult.User.Name = "brad" session := sessionCookie(t, "brad", "jf-token") t.Run("missing session", func(t *testing.T) { ev := handler.HookEvent{} if err := processCompletedUpload(ev, um, jf); err == nil { t.Fatalf("expected missing session error") } }) t.Run("missing mapping", func(t *testing.T) { ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{sessionCookie(t, "nobody", "token")}}}, Upload: handler.FileInfo{ ID: "missing-mapping", MetaData: map[string]string{ "filename": "clip.mp4", "desc": "Trip", "lib": "library", }, }, } if err := processCompletedUpload(ev, um, jf); err == nil { t.Fatalf("expected missing mapping error") } }) t.Run("missing library root isolated", func(t *testing.T) { origMediaRoot := mediaRoot origTusDir := tusDir defer func() { mediaRoot = origMediaRoot tusDir = origTusDir }() mediaRoot = filepath.Join(t.TempDir(), "media") tusDir = filepath.Join(t.TempDir(), "tus") if err := os.MkdirAll(tusDir, 0o2775); err != nil { t.Fatalf("mkdir tus dir: %v", err) } id := "missing-root-isolated" if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil { t.Fatalf("write tus payload: %v", err) } ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}}, Upload: handler.FileInfo{ ID: id, MetaData: map[string]string{ "filename": "clip.mp4", "desc": "Trip", "lib": "library", }, }, } if err := processCompletedUpload(ev, um, jf); err == nil { t.Fatalf("expected missing library root error") } }) t.Run("missing description for video", func(t *testing.T) { if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil { t.Fatalf("mkdir media root: %v", err) } id := "video-no-desc" if err := os.MkdirAll(tusDir, 0o2775); err != nil { t.Fatalf("mkdir tus dir: %v", err) } if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil { t.Fatalf("write tus payload: %v", err) } ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}}, Upload: handler.FileInfo{ ID: id, MetaData: map[string]string{ "filename": "video.mp4", "lib": "library", }, }, } if err := processCompletedUpload(ev, um, jf); err == nil { t.Fatalf("expected missing desc error") } }) t.Run("missing library root", func(t *testing.T) { id := "missing-root" ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}}, Upload: handler.FileInfo{ ID: id, MetaData: map[string]string{ "filename": "clip.mp4", "desc": "Trip", "lib": "library", }, }, } if err := processCompletedUpload(ev, um, jf); err == nil { t.Fatalf("expected missing root error") } }) t.Run("subdir mkdir failure", func(t *testing.T) { if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil { t.Fatalf("mkdir media root: %v", err) } blocker := filepath.Join(mediaRoot, "library", "bad") if err := os.WriteFile(blocker, []byte("file"), 0o644); err != nil { t.Fatalf("write blocker file: %v", err) } id := "subdir-failure" if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil { t.Fatalf("write tus payload: %v", err) } ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}}, Upload: handler.FileInfo{ ID: id, MetaData: map[string]string{ "filename": "clip.mp4", "desc": "Trip", "lib": "library", "subdir": "bad/child", }, }, } if err := processCompletedUpload(ev, um, jf); err == nil { t.Fatalf("expected mkdir failure") } }) t.Run("non-video defaults description", func(t *testing.T) { if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil { t.Fatalf("mkdir media root: %v", err) } id := "image-default-desc" if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil { t.Fatalf("write tus payload: %v", err) } ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}}, Upload: handler.FileInfo{ ID: id, MetaData: map[string]string{ "filename": "photo.jpg", "lib": "library", }, }, } if err := processCompletedUpload(ev, um, jf); err != nil { t.Fatalf("expected default-desc upload to succeed: %v", err) } expected := filepath.Join(mediaRoot, "library", composeFinalName("", "upload", "photo.jpg")) if _, err := os.Stat(expected); err != nil { t.Fatalf("expected defaulted upload output: %v", err) } }) t.Run("missing filename falls back to upload.bin", func(t *testing.T) { id := "missing-filename" if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil { t.Fatalf("write tus payload: %v", err) } ev := handler.HookEvent{ HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}}, Upload: handler.FileInfo{ ID: id, MetaData: map[string]string{ "desc": "Trip", "lib": "library", }, }, } if err := processCompletedUpload(ev, um, jf); err != nil { t.Fatalf("expected upload with fallback filename to succeed: %v", err) } expected := filepath.Join(mediaRoot, "library", composeFinalName("", "Trip", "upload.bin")) if _, err := os.Stat(expected); err != nil { t.Fatalf("expected fallback filename output: %v", err) } }) }