From b33b941d0e518a0b4db78f92a3d7771bba7ba7e8 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 13 Feb 2026 16:38:00 -0500 Subject: [PATCH] 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 --- cmd/server/tests/kobo_test.go | 17 ++--- cmd/server/tests/opds_test.go | 118 +++++++++++++++++++++------------- 2 files changed, 81 insertions(+), 54 deletions(-) diff --git a/cmd/server/tests/kobo_test.go b/cmd/server/tests/kobo_test.go index 7f3fa3d..bb017c8 100644 --- a/cmd/server/tests/kobo_test.go +++ b/cmd/server/tests/kobo_test.go @@ -1,4 +1,4 @@ -package bookhoard/cmd/server/tests +package main import ( "bookhoard/internal/handlers" @@ -31,8 +31,7 @@ func TestKoboInitialization(t *testing.T) { deviceSetup := setupDeviceTest(t) koboDevice := deviceSetup.CreateDevice(t, "Test Kobo", "kobo", "kobo-clara-test") - req, _ := http.NewRequest("GET", setup.Server.URL+"/api/sync/kobo/v1/initialization", nil) - req.Header.Set("Authorization", "Bearer "+koboDevice.AuthToken) + req, _ := http.NewRequest("GET", setup.Server.URL+"/api/sync/kobo/"+koboDevice.AuthToken+"/v1/initialization", nil) req.Header.Set("x-kobo-device", fmt.Sprintf(`{"DeviceId":"%s","Model":"Kobo Clara","SerialNumber":"%s"}`, koboDevice.ID.String(), koboDevice.Identifier)) @@ -61,8 +60,7 @@ func TestKoboLibrarySync(t *testing.T) { deviceSetup := setupDeviceTest(t) koboDevice := deviceSetup.CreateDevice(t, "Test Kobo", "kobo", "kobo-clara-test") - req, _ := http.NewRequest("GET", setup.Server.URL+"/api/sync/kobo/v1/initialization", nil) - req.Header.Set("Authorization", "Bearer "+koboDevice.AuthToken) + req, _ := http.NewRequest("GET", setup.Server.URL+"/api/sync/kobo/"+koboDevice.AuthToken+"/v1/initialization", nil) req.Header.Set("x-kobo-device", fmt.Sprintf(`{"DeviceId":"%s","Model":"Kobo Clara","SerialNumber":"%s"}`, koboDevice.ID.String(), koboDevice.Identifier)) @@ -126,9 +124,8 @@ func TestKoboMarkupSync(t *testing.T) { } body, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", setup.Server.URL+"/api/sync/kobo/markup", bytes.NewReader(body)) + req, _ := http.NewRequest("POST", setup.Server.URL+"/api/sync/kobo/"+koboDevice.AuthToken+"/markup", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+koboDevice.AuthToken) req.Header.Set("x-kobo-device", fmt.Sprintf(`{"DeviceId":"%s","Model":"Kobo Clara","SerialNumber":"%s"}`, koboDevice.ID.String(), koboDevice.Identifier)) @@ -181,9 +178,8 @@ func TestKoboBookmarkSync(t *testing.T) { } body, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", setup.Server.URL+"/api/sync/kobo/bookmark", bytes.NewReader(body)) + req, _ := http.NewRequest("POST", setup.Server.URL+"/api/sync/kobo/"+koboDevice.AuthToken+"/bookmark", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+koboDevice.AuthToken) req.Header.Set("x-kobo-device", fmt.Sprintf(`{"DeviceId":"%s","Model":"Kobo Clara","SerialNumber":"%s"}`, koboDevice.ID.String(), koboDevice.Identifier)) @@ -225,9 +221,8 @@ func TestKoboAnalyticsGettests(t *testing.T) { } body, _ := json.Marshal(reqBody) - req, _ := http.NewRequest("POST", setup.Server.URL+"/api/sync/kobo/v1/analytics/gettests", bytes.NewReader(body)) + req, _ := http.NewRequest("POST", setup.Server.URL+"/api/sync/kobo/"+koboDevice.AuthToken+"/v1/analytics/gettests", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+koboDevice.AuthToken) req.Header.Set("x-kobo-device", fmt.Sprintf(`{"DeviceId":"%s","Model":"Kobo Clara","SerialNumber":"%s"}`, koboDevice.ID.String(), koboDevice.Identifier)) diff --git a/cmd/server/tests/opds_test.go b/cmd/server/tests/opds_test.go index 03b6dc0..5eb826b 100644 --- a/cmd/server/tests/opds_test.go +++ b/cmd/server/tests/opds_test.go @@ -13,6 +13,7 @@ import ( 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) { @@ -38,18 +39,36 @@ func TestOPDSEndpoints(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) - t.Run("GetDeviceCatalog_ValidDevice", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/catalog", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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 either 200 (OK with empty catalog) or 404 (device not found) - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound) + // 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) { @@ -63,17 +82,19 @@ func TestOPDSEndpoints(t *testing.T) { }) t.Run("SearchDeviceCatalog_ValidDevice", func(t *testing.T) { - deviceID := uuid.New() + // 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/"+deviceID.String()+"/search?q=test", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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 or 404 - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound) + // Should return 200 (even if empty results) + assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("GetDeviceNavigation_InvalidDeviceID", func(t *testing.T) { @@ -87,10 +108,11 @@ func TestOPDSEndpoints(t *testing.T) { }) t.Run("GetDeviceNavigation_ValidDevice", func(t *testing.T) { - deviceID := uuid.New() + deviceSetup := setupDeviceTest(t) + device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-nav-test") - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/nav", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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) @@ -122,11 +144,12 @@ func TestOPDSEndpoints(t *testing.T) { }) t.Run("DownloadBook_ValidIDs", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/download/"+bookID, nil) - httpReq.Header.Set("Authorization", "Bearer "+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) @@ -158,11 +181,12 @@ func TestOPDSEndpoints(t *testing.T) { }) t.Run("GetCoverImage_ValidIDs", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/cover/"+bookID, nil) - httpReq.Header.Set("Authorization", "Bearer "+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) @@ -184,11 +208,12 @@ func TestOPDSEndpoints(t *testing.T) { }) t.Run("ListFormats_ValidDeviceID", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/formats/"+bookID, nil) - httpReq.Header.Set("Authorization", "Bearer "+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) @@ -206,12 +231,13 @@ func TestOPDSConversion(t *testing.T) { client := &http.Client{} t.Run("DownloadKEPUB_FormatParameter", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/download/"+bookID+"?format=kepub", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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) @@ -223,12 +249,13 @@ func TestOPDSConversion(t *testing.T) { }) t.Run("DownloadEPUB_DefaultFormat", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/download/"+bookID, nil) - httpReq.Header.Set("Authorization", "Bearer "+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) @@ -239,12 +266,13 @@ func TestOPDSConversion(t *testing.T) { }) t.Run("Download_UnsupportedFormat", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/download/"+bookID+"?format=pdf", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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) @@ -259,42 +287,46 @@ func TestOPDSConversion(t *testing.T) { 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) { - deviceID := uuid.New() + deviceSetup := setupDeviceTest(t) + device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-edge-empty") - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/catalog", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound) + assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("Search_SpecialCharacters", func(t *testing.T) { - deviceID := uuid.New() + 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/"+deviceID.String()+"/search?q=test%20%26%20more", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound) + assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("Search_EmptyQuery", func(t *testing.T) { - deviceID := uuid.New() + deviceSetup := setupDeviceTest(t) + device := deviceSetup.CreateDevice(t, "Test OPDS Device", "koreader", "opds-edge-emptyq") - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?q=", nil) - httpReq.Header.Set("Authorization", "Bearer "+token) + 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)