Files
bookhoard/cmd/server/tests/opds_test.go
T
john-okeefe b33b941d0e test: update device authentication tests
- Change Kobo sync endpoints to use URL token authentication
- Update OPDS tests to use device tokens instead of user tokens
- Support both Bearer and query parameter authentication methods
- Return error when test user already exists instead of deleting
- Prevent test interference from cleanup operations
- Improve test isolation and reliability
2026-02-13 16:38:00 -05:00

339 lines
12 KiB
Go

package main
import (
"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)
token := loginTestUser(t, setup.Server, setup.DB)
_ = createTestMediaItemID(t, setup.Server, token)
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")
bookID := createTestMediaItemID(t, setup.Server, token)
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID, 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")
bookID := createTestMediaItemID(t, setup.Server, token)
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/cover/"+bookID, 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")
bookID := createTestMediaItemID(t, setup.Server, token)
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/formats/"+bookID, 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)
token := loginTestUser(t, setup.Server, setup.DB)
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")
bookID := createTestMediaItemID(t, setup.Server, token)
// Request KEPUB format
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID+"?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")
bookID := createTestMediaItemID(t, setup.Server, token)
// Request default format (no format parameter)
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID, 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")
bookID := createTestMediaItemID(t, setup.Server, token)
// Request unsupported format
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+device.ID.String()+"/download/"+bookID+"?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)
token := loginTestUser(t, setup.Server, setup.DB)
_ = token // Used for creating media items in device 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)
})
}