mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 19:39:19 +00:00
* Refactored code; temp disable workflows on push * remove printf leftovers * A better sanitizing code * extend auth routes tests * more tests, fix 2 bugs in token processing * more refactoring and fixes * optimize video mediaURLs DB query and make testCachePath in utils thread-safe * fix always passing test * and more refactoring; 1 more test, but cleanup still has to be fixed * more debug code in the test * fix the test * Final commit * Adding a defer function just in case of panic * Address some of review comments * Protect the shared var by mutex; implement correct context-aware scanning * Better error; better initial func wrap/capture; better log messages in `photos.go`; * Addressing a few more review comments * Order the query results; a better error type catch; setting the MP4 content type header explicitly * try to fix the cancelation detection condition * Extract a function and call it by name --------- Co-authored-by: Konstantin Koval
472 lines
15 KiB
Go
472 lines
15 KiB
Go
package routes
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/photoview/photoview/api/graphql/models"
|
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
|
"github.com/photoview/photoview/api/test_utils"
|
|
"github.com/photoview/photoview/api/utils"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// setTestCachePath temporarily sets a different media cache path for testing
|
|
// and returns a function to restore the original state
|
|
func setTestCachePath(tempPath string) func() {
|
|
original := utils.GetTestCachePath()
|
|
utils.ConfigureTestCache(tempPath)
|
|
return func() {
|
|
utils.ConfigureTestCache(original)
|
|
}
|
|
}
|
|
|
|
// mockProcessSingleMedia replaces scanner.ProcessSingleMedia with a mock function during tests
|
|
// and returns a function to restore the original implementation
|
|
var originalProcessSingleMedia = processSingleMediaFn
|
|
|
|
func mockProcessSingleMedia(t *testing.T, shouldSucceed bool, mediaID int, albumID int) func() {
|
|
// Save original implementation
|
|
savedFn := processSingleMediaFn
|
|
|
|
// Replace with mock implementation
|
|
processSingleMediaFn = func(ctx context.Context, db *gorm.DB, media *models.Media) error {
|
|
// Check if context is already cancelled before starting work
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if shouldSucceed {
|
|
// On success: create the expected video file in cache
|
|
var mediaURLs []models.MediaURL
|
|
if err := db.Where("media_id = ? AND purpose = ?", media.ID, models.VideoWeb).
|
|
Find(&mediaURLs).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(mediaURLs) == 0 {
|
|
return fmt.Errorf("no media URLs found")
|
|
}
|
|
|
|
// Get the cache path
|
|
tempCachePath := utils.GetTestCachePath()
|
|
albumDir := filepath.Join(tempCachePath, strconv.Itoa(albumID))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(mediaID))
|
|
if err := os.MkdirAll(mediaDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
videoPath := filepath.Join(mediaDir, mediaURLs[0].MediaName)
|
|
if err := os.WriteFile(videoPath, []byte("mocked processed video content"), 0644); err != nil {
|
|
return fmt.Errorf("failed to write mock video file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// On failure: return an error
|
|
return fmt.Errorf("mock processing error")
|
|
}
|
|
|
|
// Return cleanup function
|
|
return func() {
|
|
processSingleMediaFn = savedFn
|
|
}
|
|
}
|
|
|
|
func registerMockVideoRoutesForTesting(db *gorm.DB, router *mux.Router, tempCachePath string) {
|
|
router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
|
|
mediaName := mux.Vars(r)["name"]
|
|
|
|
// Use no-op auth and test cache path
|
|
handleVideoRequest(
|
|
w, r, db, mediaName,
|
|
// Skip authentication for tests
|
|
func(media *models.Media, db *gorm.DB, r *http.Request) (bool, string, int, error) {
|
|
return true, "success", http.StatusOK, nil
|
|
},
|
|
// Use test cache path
|
|
func(albumID, mediaID int, filename string) string {
|
|
return path.Join(tempCachePath, strconv.Itoa(albumID), strconv.Itoa(mediaID), filename)
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
// createTestResources creates all the necessary test resources for a single test case
|
|
// and returns cleanup functions to be called with t.Cleanup()
|
|
func createTestResources(t *testing.T, db *gorm.DB, testID string) (
|
|
*models.User,
|
|
*models.Album,
|
|
*models.Media,
|
|
*models.MediaURL,
|
|
string, // mediaName
|
|
string, // cachePath
|
|
string, // shareToken
|
|
string, // tokenPassword
|
|
) {
|
|
// Create test user with unique username
|
|
user := &models.User{
|
|
Username: fmt.Sprintf("testuser-%s", testID),
|
|
}
|
|
require.NoError(t, db.Create(user).Error)
|
|
t.Cleanup(func() {
|
|
db.Unscoped().Delete(user)
|
|
})
|
|
|
|
// Create test album with unique title and path
|
|
album := &models.Album{
|
|
Title: fmt.Sprintf("Test Album %s", testID),
|
|
Path: fmt.Sprintf("/test/album/path/%s", testID),
|
|
}
|
|
require.NoError(t, db.Create(album).Error)
|
|
t.Cleanup(func() {
|
|
db.Unscoped().Delete(album)
|
|
})
|
|
|
|
// Establish ownership via many-to-many relationship
|
|
require.NoError(t, db.Model(album).Association("Owners").Append(user))
|
|
t.Cleanup(func() {
|
|
db.Model(album).Association("Owners").Clear()
|
|
})
|
|
|
|
// Create unique media name for this test
|
|
mediaName := fmt.Sprintf("video-%s.mp4", testID)
|
|
|
|
// Create media with VideoWeb purpose
|
|
media := &models.Media{
|
|
Title: fmt.Sprintf("Test Video %s", testID),
|
|
Path: filepath.Join(t.TempDir(), mediaName),
|
|
PathHash: fmt.Sprintf("testhash-%s", testID),
|
|
AlbumID: album.ID,
|
|
Album: *album,
|
|
DateShot: time.Now(),
|
|
Type: "video",
|
|
}
|
|
require.NoError(t, db.Create(media).Error)
|
|
t.Cleanup(func() {
|
|
db.Unscoped().Delete(media)
|
|
})
|
|
|
|
// Create media URL entry
|
|
mediaURL := &models.MediaURL{
|
|
MediaID: media.ID,
|
|
Media: media,
|
|
MediaName: mediaName,
|
|
Width: 1920,
|
|
Height: 1080,
|
|
Purpose: models.VideoWeb,
|
|
ContentType: "video/mp4",
|
|
FileSize: 1024,
|
|
}
|
|
require.NoError(t, db.Create(mediaURL).Error)
|
|
t.Cleanup(func() {
|
|
db.Unscoped().Delete(mediaURL)
|
|
})
|
|
|
|
// Create a unique cache path for this test
|
|
cachePath := filepath.Join(t.TempDir(), fmt.Sprintf("cache-%s", testID))
|
|
require.NoError(t, os.MkdirAll(cachePath, 0755))
|
|
t.Cleanup(func() {
|
|
os.RemoveAll(cachePath)
|
|
})
|
|
|
|
// Prepare share token for auth tests
|
|
tokenPassword := fmt.Sprintf("secret-password-%s", testID)
|
|
expiry := time.Now().Add(24 * time.Hour)
|
|
shareToken, err := actions.AddMediaShare(db, user, media.ID, &expiry, &tokenPassword)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
db.Unscoped().Delete(shareToken)
|
|
})
|
|
|
|
return user, album, media, mediaURL, mediaName, cachePath, shareToken.Value, tokenPassword
|
|
}
|
|
|
|
func TestVideoRoutes(t *testing.T) {
|
|
// Ensure original function is always restored
|
|
defer func() {
|
|
processSingleMediaFn = originalProcessSingleMedia
|
|
}()
|
|
|
|
// Setup test database
|
|
db := test_utils.DatabaseTest(t)
|
|
|
|
// Define test cases
|
|
testCases := []struct {
|
|
name string
|
|
testFunc func(*testing.T, *gorm.DB)
|
|
}{
|
|
{
|
|
name: "Valid video retrieval",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
// Create unique resources for this test
|
|
_, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "valid")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Create cache directory and file
|
|
albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID)))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID)))
|
|
require.NoError(t, os.MkdirAll(mediaDir, 0755))
|
|
|
|
videoPath := filepath.Join(mediaDir, mediaName)
|
|
require.NoError(t, os.WriteFile(videoPath, []byte("test video content"), 0644))
|
|
|
|
// Create mock router without auth for this test
|
|
router := mux.NewRouter()
|
|
registerMockVideoRoutesForTesting(db, router, cachePath)
|
|
|
|
// Make request
|
|
req := httptest.NewRequest("GET", "/"+mediaName, nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
// Validate response
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
assert.Equal(t, "test video content", rr.Body.String())
|
|
},
|
|
},
|
|
{
|
|
name: "Video not found",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
// Create unique resources for this test
|
|
_, _, _, _, _, cachePath, _, _ := createTestResources(t, db, "notfound")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Create mock router without auth for this test
|
|
router := mux.NewRouter()
|
|
registerMockVideoRoutesForTesting(db, router, cachePath)
|
|
|
|
// Make request with nonexistent video name
|
|
req := httptest.NewRequest("GET", "/nonexistent.mp4", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
// Validate response
|
|
assert.Equal(t, http.StatusNotFound, rr.Code)
|
|
assert.Equal(t, "not found", rr.Body.String())
|
|
},
|
|
},
|
|
{
|
|
name: "Authentication with share token",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
// Create unique resources for this test
|
|
_, album, media, _, mediaName, cachePath, tokenValue, tokenPassword := createTestResources(t, db, "auth")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Create the file in cache
|
|
albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID)))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID)))
|
|
require.NoError(t, os.MkdirAll(mediaDir, 0755))
|
|
|
|
videoPath := filepath.Join(mediaDir, mediaName)
|
|
require.NoError(t, os.WriteFile(videoPath, []byte("test video content"), 0644))
|
|
|
|
// Create real router with auth for this test
|
|
router := mux.NewRouter()
|
|
RegisterVideoRoutes(db, router)
|
|
|
|
// Make request with token
|
|
req := httptest.NewRequest("GET", "/"+mediaName+"?token="+tokenValue, nil)
|
|
cookie := http.Cookie{
|
|
Name: fmt.Sprintf("share-token-pw-%s", tokenValue),
|
|
Value: tokenPassword,
|
|
}
|
|
req.AddCookie(&cookie)
|
|
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
// Validate response
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
assert.Equal(t, "test video content", rr.Body.String())
|
|
},
|
|
},
|
|
{
|
|
name: "Multiple media URLs with same name",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
// Create unique resources for this test
|
|
_, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "multiple")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Create second mediaURL with same name
|
|
mediaURL2 := &models.MediaURL{
|
|
MediaID: media.ID,
|
|
Media: media,
|
|
MediaName: mediaName, // Same name
|
|
Width: 1280,
|
|
Height: 720,
|
|
Purpose: models.VideoWeb,
|
|
ContentType: "video/mp4",
|
|
FileSize: 512,
|
|
}
|
|
require.NoError(t, db.Create(mediaURL2).Error)
|
|
t.Cleanup(func() {
|
|
db.Unscoped().Delete(mediaURL2)
|
|
})
|
|
|
|
// Create cache directory and file
|
|
albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID)))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID)))
|
|
require.NoError(t, os.MkdirAll(mediaDir, 0755))
|
|
|
|
videoPath := filepath.Join(mediaDir, mediaName)
|
|
require.NoError(t, os.WriteFile(videoPath, []byte("test video content"), 0644))
|
|
|
|
// Create mock router without auth for this test
|
|
router := mux.NewRouter()
|
|
registerMockVideoRoutesForTesting(db, router, cachePath)
|
|
|
|
// Make request
|
|
req := httptest.NewRequest("GET", "/"+mediaName, nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
// Validate response
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
assert.Equal(t, "test video content", rr.Body.String())
|
|
},
|
|
},
|
|
{
|
|
name: "Video file not in cache, processing succeeds",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
// Create unique resources for this test
|
|
_, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "process-success")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Ensure cache directory exists but file doesn't exist
|
|
albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID)))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID)))
|
|
require.NoError(t, os.MkdirAll(mediaDir, 0755))
|
|
|
|
// Mock processing to succeed
|
|
restoreProcessingFn := mockProcessSingleMedia(t, true, int(media.ID), int(album.ID))
|
|
t.Cleanup(restoreProcessingFn)
|
|
|
|
// Create mock router without auth for this test
|
|
router := mux.NewRouter()
|
|
registerMockVideoRoutesForTesting(db, router, cachePath)
|
|
|
|
// Make request
|
|
req := httptest.NewRequest("GET", "/"+mediaName, nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
// Validate response
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
assert.Equal(t, "mocked processed video content", rr.Body.String())
|
|
},
|
|
},
|
|
{
|
|
name: "Video file not in cache, processing fails",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
// Create unique resources for this test
|
|
_, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "process-fail")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Ensure cache directory exists but file doesn't exist
|
|
albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID)))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID)))
|
|
require.NoError(t, os.MkdirAll(mediaDir, 0755))
|
|
|
|
// Mock processing to fail
|
|
restoreProcessingFn := mockProcessSingleMedia(t, false, int(media.ID), int(album.ID))
|
|
t.Cleanup(restoreProcessingFn)
|
|
|
|
// Create mock router without auth for this test
|
|
router := mux.NewRouter()
|
|
registerMockVideoRoutesForTesting(db, router, cachePath)
|
|
|
|
// Make request
|
|
req := httptest.NewRequest("GET", "/"+mediaName, nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
// Validate response
|
|
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
|
},
|
|
},
|
|
{
|
|
name: "Context cancellation during processing",
|
|
testFunc: func(t *testing.T, db *gorm.DB) {
|
|
_, album, media, _, mediaName, cachePath, _, _ := createTestResources(t, db, "cancellation")
|
|
|
|
// Setup cache path for this test
|
|
restorePath := setTestCachePath(cachePath)
|
|
t.Cleanup(restorePath)
|
|
|
|
// Ensure cache directory exists but file doesn't exist to trigger processing
|
|
albumDir := filepath.Join(cachePath, strconv.Itoa(int(album.ID)))
|
|
mediaDir := filepath.Join(albumDir, strconv.Itoa(int(media.ID)))
|
|
require.NoError(t, os.MkdirAll(mediaDir, 0755))
|
|
|
|
// Create cancellable context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Mock processing that simulates context cancellation
|
|
savedFn := processSingleMediaFn
|
|
processSingleMediaFn = func(reqCtx context.Context, db *gorm.DB, media *models.Media) error {
|
|
cancel()
|
|
return fmt.Errorf("processing interrupted by cancellation")
|
|
}
|
|
t.Cleanup(func() { processSingleMediaFn = savedFn })
|
|
|
|
// Create request with cancelled context
|
|
req := httptest.NewRequest("GET", "/video/"+mediaName, nil)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Use testing router without auth
|
|
mockRouter := mux.NewRouter().PathPrefix("/video").Subrouter()
|
|
registerMockVideoRoutesForTesting(db, mockRouter, cachePath)
|
|
|
|
mockRouter.ServeHTTP(w, req)
|
|
|
|
// When context is cancelled, processing should be cancelled
|
|
// 1. Status remains default 200 (no explicit status written)
|
|
assert.Equal(t, http.StatusOK, w.Code, "Status should remain default when context cancelled")
|
|
|
|
// 2. Response body should be empty (no video content served)
|
|
assert.Empty(t, w.Body.String(), "Response body should be empty when context cancelled")
|
|
},
|
|
},
|
|
}
|
|
|
|
// Run test cases
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.testFunc(t, db)
|
|
})
|
|
}
|
|
}
|