Changes the library names in TestOPDSSearchAcrossLibraries from: - "OPDS Lib 1" → "OPDS Test Lib 1" - "OPDS Lib 2" → "OPDS Test Lib 2" This ensures these libraries are properly cleaned up by the test cleanup logic that deletes libraries with "test" in their name. Combined with the cleanup fix in the previous commit, this ensures that all OPDS test libraries are removed after tests complete, preventing residual data in the database. Files changed: - cmd/server/tests/opds_test.go: Renamed libraries to include "test"
392 lines
14 KiB
Go
392 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestOPDSEndpoints tests OPDS (Open Publication Distribution System) endpoints
|
|
func TestOPDSEndpoints(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
// Create ALL media items needed for ALL subtests BEFORE any t.Run (following Kobo pattern)
|
|
_ = createTestMediaItemID(t, setup)
|
|
bookID1 := createTestMediaItemID(t, setup)
|
|
bookID2 := createTestMediaItemID(t, setup)
|
|
bookID3 := createTestMediaItemID(t, setup)
|
|
client := &http.Client{}
|
|
|
|
t.Run("GetDeviceCatalog_WithoutDeviceAuth", func(t *testing.T) {
|
|
deviceID := uuid.New()
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/catalog", nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// OPDS endpoints require device authentication via devices.auth_token
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetDeviceCatalog_InvalidDeviceID", func(t *testing.T) {
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/catalog", nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return 400 for invalid UUID
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetDeviceCatalog_ValidDevice_BearerToken", func(t *testing.T) {
|
|
// Create a device with auth token
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-bearer-test")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/catalog", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return 200 with catalog (even if empty)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetDeviceCatalog_ValidDevice_QueryToken", func(t *testing.T) {
|
|
// Create a device with auth token
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "kobo", "opds-query-test")
|
|
|
|
// Test query parameter authentication
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/catalog?token="+device.AuthToken, nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return 200 with catalog (even if empty)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("SearchDeviceCatalog_InvalidDeviceID", func(t *testing.T) {
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/search?q=test", nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("SearchDeviceCatalog_ValidDevice", func(t *testing.T) {
|
|
// Create a device with auth token
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-search-test")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/search?q=test", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return 200 (even if empty results)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetDeviceNavigation_InvalidDeviceID", func(t *testing.T) {
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/nav", nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetDeviceNavigation_ValidDevice", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-nav-test")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/nav", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return navigation or 404
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
|
})
|
|
|
|
t.Run("DownloadBook_InvalidDeviceID", func(t *testing.T) {
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/download/"+uuid.New().String(), nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DownloadBook_InvalidBookID", func(t *testing.T) {
|
|
deviceID := uuid.New()
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/download/invalid-uuid", nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DownloadBook_ValidIDs", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-download-test")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID1, nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// May return 404 if device/book not linked, or 500 for file not found
|
|
// Should not return 400 (invalid IDs)
|
|
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetCoverImage_InvalidDeviceID", func(t *testing.T) {
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/cover/"+uuid.New().String(), nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetCoverImage_InvalidBookID", func(t *testing.T) {
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/cover/"+uuid.New().String(), nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetCoverImage_ValidIDs", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-cover-test")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/cover/"+bookID2, nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// May return 404 if no cover, but not 400
|
|
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ListFormats_InvalidDeviceID", func(t *testing.T) {
|
|
bookID := uuid.New()
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/formats/"+bookID.String(), nil)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ListFormats_ValidDeviceID", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-formats-test")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/formats/"+bookID3, nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return formats list or 404
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
|
})
|
|
}
|
|
|
|
// TestOPDSConversion tests on-the-fly conversion for downloads
|
|
func TestOPDSConversion(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
// Create ALL media items needed for ALL subtests BEFORE any t.Run (following Kobo pattern)
|
|
_ = createTestMediaItemID(t, setup)
|
|
bookID1 := createTestMediaItemID(t, setup)
|
|
bookID2 := createTestMediaItemID(t, setup)
|
|
bookID3 := createTestMediaItemID(t, setup)
|
|
client := &http.Client{}
|
|
|
|
t.Run("DownloadKEPUB_FormatParameter", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "kobo", "opds-kepub-test")
|
|
|
|
// Request KEPUB format
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID1+"?format=kepub", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should attempt conversion (may fail if file doesn't exist)
|
|
// Important: Should not return 400 for invalid IDs
|
|
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DownloadEPUB_DefaultFormat", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-epub-test")
|
|
|
|
// Request default format (no format parameter)
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID2, nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should attempt to download original format
|
|
assert.NotEqual(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Download_UnsupportedFormat", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-unsupported-test")
|
|
|
|
// Request unsupported format
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID3+"?format=pdf", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should handle gracefully (either 400 for unsupported format or 404/500)
|
|
assert.True(t, resp.StatusCode >= 400 && resp.StatusCode < 600)
|
|
})
|
|
}
|
|
|
|
// TestOPDSEdgeCases tests edge cases for OPDS endpoints
|
|
func TestOPDSEdgeCases(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
_ = createTestMediaItemID(t, setup)
|
|
client := &http.Client{}
|
|
|
|
t.Run("Catalog_EmptyLibrary", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-edge-empty")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/catalog", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return empty catalog, not error
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Search_SpecialCharacters", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-edge-special")
|
|
|
|
// Search with special characters
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/search?q=test%20%26%20more", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should handle special characters
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Search_EmptyQuery", func(t *testing.T) {
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-edge-emptyq")
|
|
|
|
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/search?q=", nil)
|
|
httpReq.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(httpReq)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should handle empty query
|
|
assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500)
|
|
})
|
|
}
|
|
|
|
// TestOPDSSearchAcrossLibraries - verify OPDS actually searches across all libraries
|
|
func TestOPDSSearchAcrossLibraries(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
client := &http.Client{}
|
|
|
|
// Create two libraries
|
|
lib1Resp := createLibrary(t, client, setup, "OPDS Test Lib 1")
|
|
lib2Resp := createLibrary(t, client, setup, "OPDS Test Lib 2")
|
|
|
|
// Add folders
|
|
addFolderToLibrary(t, setup, lib1Resp["id"].(string), "/app/uploads")
|
|
addFolderToLibrary(t, setup, lib2Resp["id"].(string), "/app/uploads")
|
|
|
|
// Add books to each library
|
|
book1ID := createTestMediaItemIDInLibrary(t, client, setup, lib1Resp["id"].(string), "OPDS Book 1")
|
|
book2ID := createTestMediaItemIDInLibrary(t, client, setup, lib2Resp["id"].(string), "OPDS Book 2")
|
|
|
|
t.Logf("Created book1 in lib1: %s", book1ID)
|
|
t.Logf("Created book2 in lib2: %s", book2ID)
|
|
|
|
// Create device for OPDS access
|
|
deviceSetup := setupDeviceTest(t)
|
|
device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-cross-lib-test")
|
|
|
|
// Search via OPDS (no library_id parameter)
|
|
searchURL := setup.Server.URL + "/opds/devices/" + device.ID.String() + "/search?q=OPDS"
|
|
t.Logf("OPDS Search URL: %s", searchURL)
|
|
|
|
req, _ := http.NewRequest("GET", searchURL, nil)
|
|
req.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
|
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
t.Logf("OPDS Search Status: %d", resp.StatusCode)
|
|
|
|
// Read response body
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
bodyString := string(bodyBytes)
|
|
|
|
t.Logf("OPDS response length: %d bytes", len(bodyString))
|
|
|
|
if resp.StatusCode == 200 {
|
|
t.Logf("✅ SUCCESS - OPDS search returns 200 (not 404 like SearchMediaItemsUnified)")
|
|
t.Logf(" Response contains 'OPDS Book 1': %v", contains(bodyString, "OPDS Book 1"))
|
|
t.Logf(" Response contains 'OPDS Book 2': %v", contains(bodyString, "OPDS Book 2"))
|
|
} else {
|
|
t.Logf("❌ FAILED - OPDS returned %d", resp.StatusCode)
|
|
}
|
|
}
|