package app import ( "context" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "time" "github.com/gin-gonic/gin" "galo/internal/auth" "galo/internal/config" "galo/internal/sse" "galo/internal/storage" "galo/internal/store" ) func newTestServerWithFS(t *testing.T) (*storage.FS, *gin.Engine) { t.Helper() gin.SetMode(gin.TestMode) signer, err := storage.NewSigner("http://app.test", "secret", time.Hour, 10*time.Minute) if err != nil { t.Fatalf("signer: %v", err) } fs, err := storage.NewFS(t.TempDir(), signer) if err != nil { t.Fatalf("new fs: %v", err) } noop := func(context.Context) error { return nil } tm := auth.NewTokenManager("test-secret", 15*time.Minute, time.Hour) srv := New(&config.Config{}, store.Repos{}, noop, tm, fs, sse.NewHub(1)) return fs, srv.Router() } func TestServeObjectRoute(t *testing.T) { ctx := context.Background() fs, r := newTestServerWithFS(t) key := "objects/abc/original.jpg" if err := fs.Put(ctx, key, strings.NewReader("0123456789"), 10, "image/jpeg"); err != nil { t.Fatalf("put: %v", err) } raw, err := fs.URL(ctx, key) if err != nil { t.Fatalf("url: %v", err) } u, err := url.Parse(raw) if err != nil { t.Fatalf("parse: %v", err) } do := func(target string, rangeHeader string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, target, nil) if rangeHeader != "" { req.Header.Set("Range", rangeHeader) } rec := httptest.NewRecorder() r.ServeHTTP(rec, req) return rec } t.Run("valid", func(t *testing.T) { rec := do(u.Path+"?"+u.RawQuery, "") if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } if rec.Body.String() != "0123456789" { t.Fatalf("body = %q", rec.Body.String()) } if cc := rec.Header().Get("Cache-Control"); cc == "" || !strings.Contains(cc, "max-age=") { t.Fatalf("cache-control = %q, want max-age directive", cc) } }) t.Run("range", func(t *testing.T) { rec := do(u.Path+"?"+u.RawQuery, "bytes=2-5") if rec.Code != http.StatusPartialContent { t.Fatalf("status = %d, want 206", rec.Code) } if rec.Body.String() != "2345" { t.Fatalf("body = %q, want 2345", rec.Body.String()) } }) t.Run("expired", func(t *testing.T) { q := u.Query() q.Set("e", strconv.FormatInt(time.Now().Add(-time.Minute).Unix(), 10)) rec := do(u.Path+"?"+q.Encode(), "") if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } if cc := rec.Header().Get("Cache-Control"); cc != "" { t.Fatalf("cache-control = %q, want empty on error", cc) } }) t.Run("forged", func(t *testing.T) { q := u.Query() q.Set("s", "deadbeef") rec := do(u.Path+"?"+q.Encode(), "") if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } }) t.Run("missing", func(t *testing.T) { raw, err := fs.URL(ctx, "objects/def/original.jpg") if err != nil { t.Fatalf("url: %v", err) } mu, _ := url.Parse(raw) rec := do(mu.Path+"?"+mu.RawQuery, "") if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } }) }