package main import ( "io" "net/http" "net/http/httptest" "os" "path/filepath" "regexp" "testing" "time" tusd "github.com/tus/tusd/pkg/handler" "scm.bstein.dev/bstein/Pegasus/backend/internal" ) func TestSanitizeSegment(t *testing.T) { cases := map[string]string{ "": "", " Family Videos ": "Family_Videos", "photos/2026/trip": "photos", "bad#$name": "bad__name", "many spaces": "many___spaces", "../../etc/passwd": "..", } for in, want := range cases { if got := sanitizeSegment(in); got != want { t.Fatalf("sanitizeSegment(%q)=%q want=%q", in, got, want) } } } func TestContains(t *testing.T) { vals := []string{"a", "b", "c"} if !contains(vals, "b") { t.Fatalf("expected contains to return true") } if contains(vals, "z") { t.Fatalf("expected contains to return false") } } func TestSanitizeDescriptorAndStemOf(t *testing.T) { if got := sanitizeDescriptor(" cool.. file$$$ "); got != "cool_file_" { t.Fatalf("unexpected sanitized descriptor %q", got) } if got := sanitizeDescriptor(""); got != "upload" { t.Fatalf("expected upload fallback, got %q", got) } if got := stemOf("My.Video.File.mp4"); got != "My_Video_File" { t.Fatalf("unexpected stem %q", got) } } func TestComposeFinalName(t *testing.T) { got := composeFinalName("2026-04-09", "My Desc", "Cool.Movie.MP4") want := "2026.04.09.My_Desc.Cool_Movie.mp4" if got != want { t.Fatalf("composeFinalName got=%q want=%q", got, want) } fallback := composeFinalName("not-a-date", "", "file") if !regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}\.upload\.file\.bin$`).MatchString(fallback) { t.Fatalf("unexpected fallback final name %q", fallback) } } func TestCorsForTusHandlesPreflight(t *testing.T) { h := corsForTus(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatalf("next handler should not run for preflight") })) req := httptest.NewRequest(http.MethodOptions, "/tus/abc", nil) req.Header.Set("Origin", "https://pegasus.bstein.dev") rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Fatalf("expected 204 for preflight, got %d", rr.Code) } if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://pegasus.bstein.dev" { t.Fatalf("unexpected allow-origin %q", got) } } func TestCorsForTusPassesThrough(t *testing.T) { called := false h := corsForTus(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true w.WriteHeader(http.StatusTeapot) })) req := httptest.NewRequest(http.MethodGet, "/tus/abc", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if !called { t.Fatalf("expected wrapped handler to be called") } if rr.Code != http.StatusTeapot { t.Fatalf("expected wrapped response code, got %d", rr.Code) } } func TestSessionRequired(t *testing.T) { protected := sessionRequired(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) t.Run("unauthorized without cookie", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/tus/", nil) rr := httptest.NewRecorder() protected.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rr.Code) } }) t.Run("authorized with session cookie", func(t *testing.T) { cookieRR := httptest.NewRecorder() if err := internal.SetSession(cookieRR, "brad", "jf-token"); err != nil { t.Fatalf("SetSession failed: %v", err) } cookies := cookieRR.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected session cookie") } req := httptest.NewRequest(http.MethodGet, "/tus/", nil) req.AddCookie(cookies[0]) rr := httptest.NewRecorder() protected.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d", rr.Code) } }) } func TestClaimsFromHook(t *testing.T) { cookieRR := httptest.NewRecorder() if err := internal.SetSession(cookieRR, "alice", "jf-token"); err != nil { t.Fatalf("SetSession failed: %v", err) } cookies := cookieRR.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected session cookie") } ev := tusd.HookEvent{ HTTPRequest: tusd.HTTPRequest{Header: http.Header{ "Cookie": []string{cookies[0].Name + "=" + cookies[0].Value}, }}, } claims, err := claimsFromHook(ev) if err != nil { t.Fatalf("claimsFromHook failed: %v", err) } if claims.Username != "alice" || claims.JFToken != "jf-token" { t.Fatalf("unexpected claims %#v", claims) } _, err = claimsFromHook(tusd.HookEvent{}) if err == nil { t.Fatalf("expected missing-header error") } } func TestMoveFromTusMovesFileAndCleansSidecars(t *testing.T) { origTusDir := tusDir tusDir = t.TempDir() t.Cleanup(func() { tusDir = origTusDir }) origDryRun := internal.DryRun internal.DryRun = false t.Cleanup(func() { internal.DryRun = origDryRun }) id := "upload-id-123" src := filepath.Join(tusDir, id+".bin") if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil { t.Fatalf("write src failed: %v", err) } _ = os.WriteFile(filepath.Join(tusDir, id+".info"), []byte("meta"), 0o644) _ = os.WriteFile(filepath.Join(tusDir, id+".json"), []byte("meta"), 0o644) dst := filepath.Join(t.TempDir(), "final", "file.bin") ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: id}} if err := moveFromTus(ev, dst); err != nil { t.Fatalf("moveFromTus failed: %v", err) } b, err := os.ReadFile(dst) if err != nil { t.Fatalf("expected moved file at destination: %v", err) } if string(b) != "payload" { t.Fatalf("unexpected destination payload %q", string(b)) } if _, err := os.Stat(src); !os.IsNotExist(err) { t.Fatalf("expected source file to be removed, stat err=%v", err) } if _, err := os.Stat(filepath.Join(tusDir, id+".info")); !os.IsNotExist(err) { t.Fatalf("expected .info sidecar cleanup, stat err=%v", err) } if _, err := os.Stat(filepath.Join(tusDir, id+".json")); !os.IsNotExist(err) { t.Fatalf("expected .json sidecar cleanup, stat err=%v", err) } } func TestMoveFromTusDryRun(t *testing.T) { origTusDir := tusDir tusDir = t.TempDir() t.Cleanup(func() { tusDir = origTusDir }) origDryRun := internal.DryRun internal.DryRun = true t.Cleanup(func() { internal.DryRun = origDryRun }) id := "upload-id-dry" src := filepath.Join(tusDir, id) if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil { t.Fatalf("write src failed: %v", err) } dst := filepath.Join(t.TempDir(), "dst.bin") ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: id}} if err := moveFromTus(ev, dst); err != nil { t.Fatalf("moveFromTus dry-run failed: %v", err) } if _, err := os.Stat(src); err != nil { t.Fatalf("expected source file to remain in dry-run mode: %v", err) } if _, err := os.Stat(dst); !os.IsNotExist(err) { t.Fatalf("expected destination to be absent in dry-run mode") } } func TestMoveFromTusMissingDataFile(t *testing.T) { origTusDir := tusDir tusDir = t.TempDir() t.Cleanup(func() { tusDir = origTusDir }) ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: "missing"}} err := moveFromTus(ev, filepath.Join(t.TempDir(), "dst.bin")) if err == nil { t.Fatalf("expected not-found error") } } func TestWriteJSON(t *testing.T) { rr := httptest.NewRecorder() writeJSON(rr, map[string]any{"ok": true}) if ct := rr.Header().Get("Content-Type"); ct != "application/json" { t.Fatalf("unexpected content-type %q", ct) } if rr.Code != http.StatusOK { t.Fatalf("unexpected status %d", rr.Code) } body, _ := io.ReadAll(rr.Result().Body) if !regexp.MustCompile(`"ok"\s*:\s*true`).Match(body) { t.Fatalf("unexpected JSON body %q", string(body)) } } func TestEnvHelper(t *testing.T) { t.Setenv("PEGASUS_TEST_ENV", "value") if got := env("PEGASUS_TEST_ENV", "fallback"); got != "value" { t.Fatalf("unexpected env result %q", got) } if got := env("PEGASUS_TEST_ENV_MISSING", "fallback"); got != "fallback" { t.Fatalf("unexpected fallback env result %q", got) } } func TestLoggingRWWriteHeader(t *testing.T) { rr := httptest.NewRecorder() lw := &loggingRW{ResponseWriter: rr, status: http.StatusOK} lw.WriteHeader(http.StatusCreated) if lw.status != http.StatusCreated { t.Fatalf("expected tracked status %d, got %d", http.StatusCreated, lw.status) } if rr.Code != http.StatusCreated { t.Fatalf("expected recorder status %d, got %d", http.StatusCreated, rr.Code) } } func TestComposeFinalNameDateFallbackNotEmpty(t *testing.T) { got := composeFinalName("", "clip", "v.mkv") today := time.Now().Format("2006.01.02") if got[:10] != today { t.Fatalf("expected date prefix %s got %s", today, got[:10]) } }