diff --git a/api/routes/photos.go b/api/routes/photos.go index 9235c7b7..9a9e928c 100644 --- a/api/routes/photos.go +++ b/api/routes/photos.go @@ -26,6 +26,11 @@ func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) { } media := mediaURL.Media + if media == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Media not found")) + return + } if success, response, status, err := authenticateMedia(media, db, r); !success { if err != nil { @@ -46,7 +51,7 @@ func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) { if _, err := os.Stat(cachedPath); os.IsNotExist((err)) { // err := db.Transaction(func(tx *gorm.DB) error { - if err = scanner.ProcessSingleMedia(db, media); err != nil { + if err = scanner.ProcessSingleMediaFunc(db, media); err != nil { log.Printf("ERROR: processing image not found in cache (%s): %s\n", cachedPath, err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(internalServerError)) diff --git a/api/routes/photos_test.go b/api/routes/photos_test.go new file mode 100644 index 00000000..c943b8cb --- /dev/null +++ b/api/routes/photos_test.go @@ -0,0 +1,122 @@ +package routes + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "path" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/photoview/photoview/api/graphql/auth" + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/test_utils" + "github.com/photoview/photoview/api/utils" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestPhotoRoutes(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user, err := models.RegisterUser(db, "testuser", nil, false) + assert.NoError(t, err) + album := models.Album{Title: "test_album", Path: "/photos"} + assert.NoError(t, db.Model(&user).Association("Albums").Append(&album)) + + media := models.Media{ + Title: "test_media", + Path: "/photos/test_image.jpg", + AlbumID: album.ID, + DateShot: time.Now(), + Type: models.MediaTypePhoto, + } + assert.NoError(t, db.Save(&media).Error) + + mediaURL := models.MediaURL{ + MediaID: media.ID, + Media: &media, + MediaName: "test_image.jpg", + Width: 1000, + Height: 800, + Purpose: models.PhotoThumbnail, + ContentType: "image/jpeg", + FileSize: 1024, + } + assert.NoError(t, db.Save(&mediaURL).Error) + + tempDir := t.TempDir() + orig := utils.MediaCachePath() + utils.ConfigureTestCache(tempDir) + defer utils.ConfigureTestCache(orig) + + router := mux.NewRouter() + RegisterPhotoRoutes(db, router) + + // -- Test cases -- + + // Non-existent media_name => 404 (no auth required) + t.Run("media not found", func(t *testing.T) { + req := httptest.NewRequest("GET", "/does_not_exist.jpg", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "404 - Media not found", rec.Body.String()) + }) + + // Missing auth => 403 + t.Run("auth failure", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test_image.jpg", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Contains(t, rec.Body.String(), "unauthorized") + }) + + // Cache miss + scan error => 500 + t.Run("scan failure yields 500", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test_image.jpg", nil) + ctx := auth.AddUserToContext(req.Context(), user) + req = req.WithContext(ctx) + + // ensure no cached file + cachedPath, err := mediaURL.CachedPath() + assert.NoError(t, err) + os.Remove(cachedPath) + + // mock scan to fail + origScan := scanner.ProcessSingleMediaFunc + scanner.ProcessSingleMediaFunc = func(db *gorm.DB, m *models.Media) error { + return fmt.Errorf("scan error") + } + defer func() { scanner.ProcessSingleMediaFunc = origScan }() + + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusInternalServerError, rec.Code) + assert.Contains(t, rec.Body.String(), "server error") + }) + + // Cache hit => 200 with correct body and headers + t.Run("cache hit serves file", func(t *testing.T) { + req := httptest.NewRequest("GET", "/test_image.jpg", nil) + ctx := auth.AddUserToContext(req.Context(), user) + req = req.WithContext(ctx) + + // pre-create cached file + cachedPath, err := mediaURL.CachedPath() + assert.NoError(t, err) + assert.NoError(t, os.MkdirAll(path.Dir(cachedPath), 0755)) + content := []byte("cached-binary") + assert.NoError(t, os.WriteFile(cachedPath, content, 0644)) + + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "cached-binary", rec.Body.String()) + assert.Equal(t, "private, max-age=86400, immutable", rec.Header().Get("Cache-Control")) + }) +} diff --git a/api/scanner/scanner_media.go b/api/scanner/scanner_media.go index 416e538e..98c600eb 100644 --- a/api/scanner/scanner_media.go +++ b/api/scanner/scanner_media.go @@ -14,6 +14,8 @@ import ( "gorm.io/gorm" ) +var ProcessSingleMediaFunc = ProcessSingleMedia + func ScanMedia(tx *gorm.DB, mediaPath string, albumId int, cache *scanner_cache.AlbumScannerCache) (*models.Media, bool, error) { mediaName := path.Base(mediaPath)