From 7ecfbcdb73c9c2174896a5df7dc03374445f0505 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 24 Mar 2026 16:47:49 -0400 Subject: [PATCH] test: add cross-library search verification for OPDS Adds TestOPDSSearchAcrossLibraries function to verify that OPDS search endpoint works across multiple libraries. Test creates: 1. Two separate libraries with unique IDs 2. Books in each library (OPDS Book 1, OPDS Book 2) 3. Test device for OPDS authentication 4. Searches without library_id parameter Test validates that: - OPDS returns 200 (not 404) - Response contains both books from different libraries - Cross-library search functionality works as expected This test served as verification that the SQL NULL handling pattern used by OPDS (2-part check) works correctly for cross-library searches. --- cmd/server/tests/opds_test.go | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/cmd/server/tests/opds_test.go b/cmd/server/tests/opds_test.go index d35c9ce..1786c57 100644 --- a/cmd/server/tests/opds_test.go +++ b/cmd/server/tests/opds_test.go @@ -1,6 +1,7 @@ package main import ( + "io" "net/http" "testing" @@ -336,3 +337,55 @@ func TestOPDSEdgeCases(t *testing.T) { 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 Lib 1") + lib2Resp := createLibrary(t, client, setup, "OPDS 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) + } +}