Files
bookhoard/cmd/server/tests/test_helpers.go
T
john-okeefe ab11eade68 refactor: inject ConnectionManager into Worker
Pass ConnectionManager to Worker constructor to enable WebSocket
broadcasting capabilities. Updated:
- main.go: server initialization
- test_helpers.go: test setup
- commonhandlers.go: handler initialization

This change enables Worker to broadcast job updates to connected clients.
2026-03-05 17:13:26 -05:00

721 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bookhoard/internal/config"
"bookhoard/internal/database"
"bookhoard/internal/handlers"
"bookhoard/internal/middleware"
ratelimit "bookhoard/internal/middleware"
"bookhoard/internal/router"
"bookhoard/internal/services"
wsync "bookhoard/internal/sync"
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
"github.com/stretchr/testify/require"
)
// CustomValidator wraps the go-playground validator
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
// TestDeviceSetup provides a complete, isolated test environment for device tests
type TestDeviceSetup struct {
Server *httptest.Server
DB *database.Queries
Config *config.Config
User UserTestData
Device DeviceTestData
Library LibraryTestData
UserToken string
}
type LibraryTestData struct {
ID string
Name string
Type string
}
type UserTestData struct {
ID uuid.UUID
Email string
Username string
Password string
Token string
}
type DeviceTestData struct {
ID uuid.UUID
Name string
Type string
Identifier string
AuthToken string
PGType database.Devices
}
// TestServerSetup manages the lifecycle of a test server with proper resource cleanup
type TestServerSetup struct {
Server *httptest.Server
DB *database.Queries
DBPool *pgxpool.Pool
Config *config.Config
ConnManager *wsync.ConnectionManager
QueueProcessor *wsync.SyncQueueProcessor
CleanupCancel context.CancelFunc
QueueCtx context.Context
QueueCancel context.CancelFunc
Token string
RegularToken string
mu sync.Mutex
closed bool
}
// Close cleans up all resources in the correct order
func (s *TestServerSetup) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil
}
// Stop queue processor first
if s.QueueCancel != nil {
s.QueueCancel()
s.QueueCancel = nil
}
// Stop connection manager cleanup task
if s.CleanupCancel != nil {
s.CleanupCancel()
s.CleanupCancel = nil
}
// Close HTTP server
if s.Server != nil {
s.Server.Close()
s.Server = nil
}
// Close database pool (this waits for all connections to be released)
if s.DBPool != nil {
s.DBPool.Close()
s.DBPool = nil
}
s.closed = true
return nil
}
// Helper functions for testing
func containsPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}
func trimSpace(s string) string {
return strings.TrimSpace(s)
}
// isRunningInContainer detects if tests are running inside a Docker container
func isRunningInContainer() bool {
// Check for container-specific marker file
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check if /app/uploads exists (container path)
if _, err := os.Stat("/app/uploads"); err == nil {
return true
}
// Check environment variable (explicit override)
if os.Getenv("TEST_IN_CONTAINER") == "true" {
return true
}
return false
}
// getUploadPath returns the appropriate upload path based on runtime environment
func getUploadPath() string {
// Check for explicit override first
if path := os.Getenv("TEST_UPLOAD_PATH"); path != "" {
return path
}
if isRunningInContainer() {
return "/app/uploads" // Container path (right side of volume mount)
}
return "./uploads" // Host path (left side of volume mount)
}
// getCachePath returns the appropriate cache path based on runtime environment
func getCachePath() string {
// Check for explicit override first
if path := os.Getenv("TEST_CACHE_PATH"); path != "" {
return path
}
if isRunningInContainer() {
return "/app/cache/kepub" // Container path (volume mount)
}
// Note: This is a Docker volume on host, not a folder
// Tests using this should handle the volume appropriately
return "/app/cache/kepub"
}
// setupDeviceTest creates a complete test environment for device tests
func setupDeviceTest(t *testing.T) *TestDeviceSetup {
serverSetup := setupTestServer(t)
// Create user ONCE with known credentials
user := createTestUserOnce(t, serverSetup.DB)
// Login to get token
token := loginUserWithCredentials(t, serverSetup.Server, user.Email, user.Password)
return &TestDeviceSetup{
Server: serverSetup.Server,
DB: serverSetup.DB,
Config: serverSetup.Config,
User: user,
UserToken: token,
}
}
// createTestUserOnce returns the pre-created test user info
func createTestUserOnce(t *testing.T, db *database.Queries) UserTestData {
ctx := context.Background()
user, err := db.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
require.NoError(t, err, "Test user should exist (created by setupTestServer)")
userUUID, err := uuid.FromBytes(user.ID.Bytes[0:16])
require.NoError(t, err, "Should parse user UUID")
return UserTestData{
ID: userUUID,
Email: "testuser@tests.bookhoard.internal",
Username: "testuser",
Password: "Test@Pass123!",
}
}
// createRegularUserOnce creates a regular (non-admin) test user with unique credentials
func createRegularUserOnce(t *testing.T, db *database.Queries) UserTestData {
ctx := context.Background()
uniqueID := uuid.New().String()[:8]
email := fmt.Sprintf("regularuser-%s@tests.bookhoard.internal", uniqueID)
username := fmt.Sprintf("regularuser-%s", uniqueID)
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
newUser, err := db.CreateUser(ctx, database.CreateUserParams{
Email: email,
Username: username,
PasswordHash: passwordHash,
FirstName: pgtype.Text{String: "Regular", Valid: true},
LastName: pgtype.Text{String: "User", Valid: true},
Role: "user",
})
require.NoError(t, err, "Should create regular test user")
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[0:16])
require.NoError(t, err, "Should parse user UUID")
createDefaultCollectionsForUser(t, db, pgtype.UUID{Bytes: userUUID, Valid: true})
t.Cleanup(func() {
ctx := context.Background()
db.DeleteUser(ctx, pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true})
})
return UserTestData{
ID: userUUID,
Email: email,
Username: username,
Password: "Test@Pass123!",
}
}
// uuidToPGType converts uuid.UUID to pgtype.UUID
func uuidToPGType(u uuid.UUID) pgtype.UUID {
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
}
// createDefaultCollectionsForUser creates the 4 default system collections for a user
func createDefaultCollectionsForUser(t *testing.T, db *database.Queries, userID pgtype.UUID) {
ctx := context.Background()
defaultCollections := []struct {
Name string
Description string
Icon string
Color string
QueryType string
Priority int32
}{
{"continue-reading", "Books you're currently reading (0 < progress < 1)", "📖", "#7aa2f7", "continue-reading", 1},
{"recently-added", "Newly added items to this library", "🆕", "#9ece6a", "recently-added", 2},
{"recently-read", "Books you've finished (progress >= 1)", "✅", "#e0af68", "recently-read", 3},
{"not-started", "Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", "not-started", 4},
}
for _, col := range defaultCollections {
_, err := db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
UserID: userID,
Name: col.Name,
Description: pgtype.Text{String: col.Description, Valid: true},
Icon: pgtype.Text{String: col.Icon, Valid: true},
Color: pgtype.Text{String: col.Color, Valid: true},
ShowOnDashboard: pgtype.Bool{Bool: true, Valid: true},
QueryType: pgtype.Text{String: col.QueryType, Valid: true},
Priority: pgtype.Int4{Int32: col.Priority, Valid: true},
})
require.NoError(t, err, "Should create default collection: "+col.Name)
}
}
// loginUserWithCredentials performs explicit login with provided credentials
func loginUserWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
loginRequest := map[string]interface{}{
"login": email,
"password": password,
}
body, _ := json.Marshal(loginRequest)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err, "Failed to login")
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed")
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
token, ok := result["access_token"].(string)
require.True(t, ok, "Should have access_token")
require.NotEmpty(t, token, "Access token should not be empty")
return token
}
// CreateDevice creates a test device for the TestDeviceSetup
func (s *TestDeviceSetup) CreateDevice(t *testing.T, deviceName, deviceType, deviceIdentifier string) *DeviceTestData {
deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true}
device, err := s.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
UserID: pgUserID,
DeviceName: deviceName,
DeviceType: deviceType,
DeviceIdentifier: deviceIdentifier,
AuthToken: deviceToken,
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
AutoSync: pgtype.Bool{Bool: true, Valid: true},
SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true},
DeviceMetadata: []byte("{}"),
})
require.NoError(t, err, "Should create device")
deviceUUID, err := uuid.FromBytes(device.ID.Bytes[0:16])
require.NoError(t, err, "Should parse device ID")
return &DeviceTestData{
ID: deviceUUID,
Name: deviceName,
Type: deviceType,
Identifier: deviceIdentifier,
AuthToken: deviceToken,
PGType: device,
}
}
// CreateLibrary creates a test library for the TestDeviceSetup
func (s *TestDeviceSetup) CreateLibrary(t *testing.T, name, libraryType string) string {
ctx := context.Background()
// Get the library_type_id for the specified type
libraryTypeRow, err := s.DB.GetLibraryTypeByName(ctx, libraryType)
require.NoError(t, err, "Should find library type")
pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true}
library, err := s.DB.CreateLibrary(ctx, database.CreateLibraryParams{
Name: name,
Description: pgtype.Text{String: "Test library description", Valid: true},
LibraryTypeID: libraryTypeRow.ID,
CreatedByAdminID: pgUserID,
})
require.NoError(t, err, "Should create library")
libraryUUID, err := uuid.FromBytes(library.ID.Bytes[0:16])
require.NoError(t, err, "Should parse library ID")
s.Library = LibraryTestData{
ID: libraryUUID.String(),
Name: name,
Type: libraryType,
}
return libraryUUID.String()
}
// CreateCollection creates a test collection for the TestDeviceSetup
func (s *TestDeviceSetup) CreateCollection(t *testing.T, name string) string {
ctx := context.Background()
pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true}
collection, err := s.DB.CreateCollection(ctx, database.CreateCollectionParams{
UserID: pgUserID,
Name: name,
Description: pgtype.Text{String: "Test collection description", Valid: true},
Color: pgtype.Text{String: "#FF5733", Valid: true},
Icon: pgtype.Text{String: "folder", Valid: true},
})
require.NoError(t, err, "Should create collection")
collectionUUID, err := uuid.FromBytes(collection.ID.Bytes[0:16])
require.NoError(t, err, "Should parse collection ID")
return collectionUUID.String()
}
// setupTestServer creates a test server with a test database
// Returns: *TestServerSetup with automatic cleanup via t.Cleanup
func setupTestServer(t *testing.T) *TestServerSetup {
// Load configuration using the same method as main application
cfg := config.LoadConfig()
// Apply test-specific overrides
cfg.ServerPort = "0" // Use random port for tests
cfg.BaseURL = "http://localhost"
cfg.JWTSecret = "test-secret-key"
cfg.UploadPath = getUploadPath()
cfg.TestMode = true
cfg.RateLimitEnabled = false
cfg.RequestsPerMinute = 1000
// Connect to test database using the same method as main application
// Use max_conns=1 to prevent connection pool exhaustion during test runs
// (78 tests × 1 connection = 78 connections, well under PostgreSQL's 100 default max_connections)
dbConfig, err := pgxpool.ParseConfig(cfg.DatabaseURL())
require.NoError(t, err, "Failed to parse database URL")
dbConfig.MaxConns = 1
dbPool, err := pgxpool.NewWithConfig(context.Background(), dbConfig)
require.NoError(t, err, "Failed to connect to test database")
queries := database.New(dbPool)
// Create login attempt tracker
loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute)
// Create handlers
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
libraryHandler := handlers.NewLibraryHandler(queries)
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
// Create WebSocket connection manager
connManager := wsync.NewConnectionManager()
cleanupCancel := connManager.StartCleanupTask()
// Create sync queue processor with cancellable context
queueProcessor := wsync.NewSyncQueueProcessor(queries)
queueCtx, queueCancel := context.WithCancel(context.Background())
go queueProcessor.Start(queueCtx)
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
conflictHandler := handlers.NewConflictHandler(queries, connManager)
analyticsHandler := handlers.NewAnalyticsHandler(queries)
queueHandler := handlers.NewQueueHandler(queries, queueProcessor)
systemSettingsHandler := handlers.NewSystemSettingsHandler(queries)
// Create refactored handlers (matching main.go)
libraryService := services.NewLibraryService(queries)
worker := services.NewWorker(3, connManager)
collectionHandler := handlers.NewCollectionHandler(queries, libraryService, connManager)
dashboardService := services.NewDashboardService(queries)
dashboardHandler := handlers.NewDashboardHandler(queries)
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
// Create conversion service for OPDS
conversionService := services.NewConversionService(queries, getCachePath())
opdsHandler := handlers.NewOPDSHandler(queries, libraryService, conversionService)
// Create Echo instance
e := echo.New()
// Set up validator
v := validator.New()
if err := ratelimit.RegisterPasswordValidation(v); err != nil {
t.Fatal("Failed to register password validator:", err)
}
e.Validator = &CustomValidator{validator: v}
// Middleware
e.Use(echomiddleware.Logger())
e.Use(echomiddleware.Recover())
e.Use(echomiddleware.CORS())
// Setup routes using router package
routerConfig := &router.Config{
Echo: e,
Queries: queries,
Cfg: cfg,
DBPool: dbPool,
AuthHandler: authHandler,
LibraryHandler: libraryHandler,
DeviceHandler: deviceHandler,
MediaHandler: mediaHandler,
MatchingHandler: matchingHandler,
KOReaderHandler: koreaderHandler,
WSHandler: wsHandler,
ConflictHandler: conflictHandler,
AnalyticsHandler: analyticsHandler,
QueueHandler: queueHandler,
SystemSettingsHandler: systemSettingsHandler,
CollectionHandler: collectionHandler,
DashboardHandler: dashboardHandler,
DashboardService: dashboardService,
OPDSHandler: opdsHandler,
ConnManager: connManager,
QueueProcessor: queueProcessor,
DeviceAuthMiddleware: deviceAuthMiddleware,
LoginTracker: loginAttemptTracker,
}
router.RegisterRoutes(routerConfig)
// Create test server
ts := httptest.NewServer(e)
ctx := context.Background()
// Delete ALL test users (any user with test email domains) to ensure clean state
// This handles users created during tests that may have been promoted to admin, etc.
allUsers, err := queries.ListUsers(ctx)
if err == nil {
for _, user := range allUsers {
if strings.HasSuffix(user.Email, "@example.com") || strings.HasSuffix(user.Email, "@tests.bookhoard.internal") {
queries.DeleteUser(ctx, user.ID)
}
}
}
// Delete test libraries (names containing "test" - case insensitive)
// This cleans up libraries created by tests while preserving user-created libraries
// NOTE: Do not use "test" in library names if you want to keep them!
allLibs, _ := queries.ListLibraries(ctx)
for _, lib := range allLibs {
if strings.Contains(strings.ToLower(lib.Name), "test") {
queries.DeleteLibrary(ctx, lib.ID)
}
}
// Create fresh admin test user
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
adminUser, err := queries.CreateUser(ctx, database.CreateUserParams{
Email: "testuser@tests.bookhoard.internal",
Username: "testuser",
PasswordHash: passwordHash,
FirstName: pgtype.Text{String: "Test", Valid: true},
LastName: pgtype.Text{String: "User", Valid: true},
Role: "admin",
})
require.NoError(t, err, "Failed to create admin test user")
adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:])
require.NoError(t, err, "Failed to parse admin user UUID")
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
// Create fresh regular test user
regularUser, err := queries.CreateUser(ctx, database.CreateUserParams{
Email: "testregularuser@tests.bookhoard.internal",
Username: "testregularuser",
PasswordHash: passwordHash,
FirstName: pgtype.Text{String: "Regular", Valid: true},
LastName: pgtype.Text{String: "User", Valid: true},
Role: "user",
})
require.NoError(t, err, "Failed to create regular test user")
regularUUID, err := uuid.FromBytes(regularUser.ID.Bytes[:])
require.NoError(t, err, "Failed to parse regular user UUID")
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: regularUUID, Valid: true})
// Login to get tokens
adminToken := loginWithCredentials(t, ts, "testuser@tests.bookhoard.internal", "Test@Pass123!")
regularToken := loginWithCredentials(t, ts, "testregularuser@tests.bookhoard.internal", "Test@Pass123!")
// Create TestServerSetup struct with all resources
setup := &TestServerSetup{
Server: ts,
DB: queries,
DBPool: dbPool,
Config: cfg,
ConnManager: connManager,
QueueProcessor: queueProcessor,
CleanupCancel: cleanupCancel,
QueueCtx: queueCtx,
QueueCancel: queueCancel,
Token: adminToken,
RegularToken: regularToken,
}
// Register cleanup function to run automatically when test completes
t.Cleanup(func() {
if err := setup.Close(); err != nil {
t.Errorf("Failed to cleanup test server: %v", err)
}
})
return setup
}
func loginWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
loginRequest := map[string]interface{}{
"login": email,
"password": password,
}
body, _ := json.Marshal(loginRequest)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err, "Failed to login")
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed")
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
token, ok := result["access_token"].(string)
require.True(t, ok, "Should have access_token")
require.NotEmpty(t, token, "Access token should not be empty")
return token
}
func getTestUserID(t *testing.T, db *database.Queries) uuid.UUID {
ctx := context.Background()
user, err := db.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
require.NoError(t, err, "Test user should exist")
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
require.NoError(t, err, "Failed to parse user UUID")
return userUUID
}
func getRegularUserID(t *testing.T, db *database.Queries) uuid.UUID {
ctx := context.Background()
user, err := db.GetUserByEmail(ctx, "testregularuser@tests.bookhoard.internal")
require.NoError(t, err, "Regular user should exist")
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
require.NoError(t, err, "Failed to parse user UUID")
return userUUID
}
// createTestMediaItemID creates a test media item and returns its ID
func createTestMediaItemID(t *testing.T, setup *TestServerSetup) string {
uniqueName := fmt.Sprintf("Test Library %d", time.Now().UnixNano())
httpClient := &http.Client{}
libReq := map[string]interface{}{
"name": uniqueName,
"description": "A test library for media items",
"type": "ebooks",
}
libBody, _ := json.Marshal(libReq)
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+setup.Token)
resp, err := httpClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusCreated, resp.StatusCode)
var libResult map[string]interface{}
json.NewDecoder(resp.Body).Decode(&libResult)
libData := libResult["id"].(string)
folderReq := map[string]interface{}{
"folder_path": "/app/uploads",
}
folderBody, _ := json.Marshal(folderReq)
folderReqHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries/"+libData+"/folders", bytes.NewBuffer(folderBody))
folderReqHTTP.Header.Set("Content-Type", "application/json")
folderReqHTTP.Header.Set("Authorization", "Bearer "+setup.Token)
folderResp, err := httpClient.Do(folderReqHTTP)
require.NoError(t, err)
defer folderResp.Body.Close()
require.Equal(t, http.StatusCreated, folderResp.StatusCode, "Library folder creation is required before adding media items")
mediaItemReq := map[string]interface{}{
"library_id": libData,
"title": "Test Media Item",
"author": "Test Author",
"file_path": "/tmp/test.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
mediaItemBody, _ := json.Marshal(mediaItemReq)
req2, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaItemBody))
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("Authorization", "Bearer "+setup.Token)
resp2, err := httpClient.Do(req2)
require.NoError(t, err)
defer resp2.Body.Close()
require.Equal(t, http.StatusCreated, resp2.StatusCode)
var mediaItemResult map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&mediaItemResult)
mediaItemID := mediaItemResult["id"].(string)
t.Cleanup(func() {
deleteReq, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/libraries/"+libData, nil)
deleteReq.Header.Set("Authorization", "Bearer "+setup.Token)
httpClient.Do(deleteReq)
})
return mediaItemID
}