306 lines
8.4 KiB
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])
|
|
}
|
|
}
|