pegasus/backend/main_test.go

306 lines
8.4 KiB
Go

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])
}
}