diff --git a/cmd/server/tests/system_settings_test.go b/cmd/server/tests/system_settings_test.go index d745b8a..37d23e9 100644 --- a/cmd/server/tests/system_settings_test.go +++ b/cmd/server/tests/system_settings_test.go @@ -8,79 +8,47 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestSystemSettingsHandler tests the system-wide scan settings endpoints func TestSystemSettingsHandler(t *testing.T) { + setup := setupTestServer(t) + t.Run("GET /api/libraries/scan-settings - Get settings without auth", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/libraries/scan-settings", nil) - rr := httptest.NewRecorder() + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(`{"message":"missing or malformed jwt"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusUnauthorized, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("GET /api/libraries/scan-settings - Get settings as non-admin", func(t *testing.T) { + token := loginRegularUser(t, setup.Server, setup.DB) + req := httptest.NewRequest("GET", "/api/libraries/scan-settings", nil) - req.Header.Set("Authorization", "Bearer valid-user-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("GET /api/libraries/scan-settings - Get settings as admin", func(t *testing.T) { + token := loginAdminUser(t, setup.Server, setup.DB) + req := httptest.NewRequest("GET", "/api/libraries/scan-settings", nil) - req.Header.Set("Authorization", "Bearer valid-admin-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - expectedResponse := map[string]interface{}{ - "scan_frequency_minutes": 60, - "auto_scan_enabled": true, - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(expectedResponse) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) var response map[string]interface{} - err := json.NewDecoder(rr.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, float64(60), response["scan_frequency_minutes"]) - assert.Equal(t, true, response["auto_scan_enabled"]) + err := json.NewDecoder(rec.Body).Decode(&response) + require.NoError(t, err) + assert.Contains(t, response, "scan_frequency_minutes") + assert.Contains(t, response, "auto_scan_enabled") }) t.Run("PUT /api/libraries/scan-settings - Update without auth", func(t *testing.T) { @@ -92,24 +60,15 @@ func TestSystemSettingsHandler(t *testing.T) { req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") - rr := httptest.NewRecorder() + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(`{"message":"missing or malformed jwt"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusUnauthorized, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("PUT /api/libraries/scan-settings - Update as non-admin", func(t *testing.T) { + token := loginRegularUser(t, setup.Server, setup.DB) + payload := map[string]interface{}{ "scan_frequency_minutes": 30, "auto_scan_enabled": true, @@ -118,25 +77,16 @@ func TestSystemSettingsHandler(t *testing.T) { req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-user-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(`{"error":"admin access required"}`)) - return - } - - w.WriteHeader(http.StatusOK) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusForbidden, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("PUT /api/libraries/scan-settings - Update with invalid frequency", func(t *testing.T) { + token := loginAdminUser(t, setup.Server, setup.DB) + testCases := []struct { name string scanFrequencyMinutes int @@ -157,27 +107,18 @@ func TestSystemSettingsHandler(t *testing.T) { req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-admin-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusBadRequest, rec.Code) }) } }) t.Run("PUT /api/libraries/scan-settings - Update with missing required field", func(t *testing.T) { + token := loginAdminUser(t, setup.Server, setup.DB) + payload := map[string]interface{}{ "auto_scan_enabled": true, } @@ -185,25 +126,16 @@ func TestSystemSettingsHandler(t *testing.T) { req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-admin-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusBadRequest, rec.Code) }) t.Run("PUT /api/libraries/scan-settings - Update with valid data", func(t *testing.T) { + token := loginAdminUser(t, setup.Server, setup.DB) + testCases := []struct { name string scanFrequencyMinutes int @@ -226,61 +158,33 @@ func TestSystemSettingsHandler(t *testing.T) { req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-admin-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - expectedResponse := map[string]interface{}{ - "scan_frequency_minutes": tc.scanFrequencyMinutes, - "auto_scan_enabled": tc.autoScanEnabled, - "message": "scan settings updated successfully", - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(expectedResponse) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) var response map[string]interface{} - err := json.NewDecoder(rr.Body).Decode(&response) - assert.NoError(t, err) + err := json.NewDecoder(rec.Body).Decode(&response) + require.NoError(t, err) assert.Equal(t, float64(tc.scanFrequencyMinutes), response["scan_frequency_minutes"]) assert.Equal(t, tc.autoScanEnabled, response["auto_scan_enabled"]) - assert.Equal(t, "scan settings updated successfully", response["message"]) }) } }) t.Run("PUT /api/libraries/scan-settings - Update with invalid JSON", func(t *testing.T) { + token := loginAdminUser(t, setup.Server, setup.DB) + invalidJSON := []byte(`{scan_frequency_minutes: 60, auto_scan_enabled: true}`) req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(invalidJSON)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-admin-token") - rr := httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userRole := r.Header.Get("X-User-Role") - if userRole != "admin" { - w.WriteHeader(http.StatusForbidden) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"invalid request"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) + setup.Server.Config.Handler.ServeHTTP(rec, req) + assert.Equal(t, http.StatusBadRequest, rec.Code) }) } @@ -303,7 +207,6 @@ func TestSystemSettingsIntegration(t *testing.T) { } assert.Equal(t, false, settings["auto_scan_enabled"]) - assert.Equal(t, "Scans should not run when auto_scan_enabled is false", "Scans should not run") }) t.Run("Valid frequency range enforcement", func(t *testing.T) { diff --git a/cmd/server/tests/test_helpers.go b/cmd/server/tests/test_helpers.go index 3a6e203..0b545c3 100644 --- a/cmd/server/tests/test_helpers.go +++ b/cmd/server/tests/test_helpers.go @@ -405,6 +405,7 @@ func setupTestServer(t *testing.T) *TestServerSetup { 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) @@ -434,26 +435,27 @@ func setupTestServer(t *testing.T) *TestServerSetup { // 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, - CollectionHandler: collectionHandler, - OPDSHandler: opdsHandler, - ConnManager: connManager, - QueueProcessor: queueProcessor, - DeviceAuthMiddleware: deviceAuthMiddleware, - LoginTracker: loginAttemptTracker, + 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, + OPDSHandler: opdsHandler, + ConnManager: connManager, + QueueProcessor: queueProcessor, + DeviceAuthMiddleware: deviceAuthMiddleware, + LoginTracker: loginAttemptTracker, } router.RegisterRoutes(routerConfig) @@ -549,6 +551,70 @@ func getTestUserID(t *testing.T, db *database.Queries) uuid.UUID { return userUUID } +func loginRegularUser(t *testing.T, ts *httptest.Server, db *database.Queries) string { + // Ensure test user exists first + _ = getRegularUserID(t, db) + + loginRequest := map[string]interface{}{ + "login": "testregularuser@example.com", + "password": "Test@Pass123!", + } + 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 test user") + 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 getRegularUserID(t *testing.T, db *database.Queries) uuid.UUID { + ctx := context.Background() + + // Check if test user exists and delete them first to ensure fresh state + user, err := db.GetUserByEmail(ctx, "testregularuser@example.com") + if err == nil { + // User exists, delete them to ensure fresh password + err = db.DeleteUser(ctx, user.ID) + if err != nil { + // If delete fails (user might be referenced elsewhere), log and continue + t.Logf("Warning: Could not delete existing test user: %v", err) + } + } + + // Create a fresh test user with a valid password + // Password: "Test@Pass123!" meets complexity requirements + // This is the bcrypt hash for "Test@Pass123!" + passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq" + + newUser, err := db.CreateUser(ctx, database.CreateUserParams{ + Email: "testregularuser@example.com", + Username: "testregularuser", + PasswordHash: passwordHash, + FirstName: pgtype.Text{String: "Test", Valid: true}, + LastName: pgtype.Text{String: "User", Valid: true}, + Role: "user", + }) + require.NoError(t, err, "Failed to create test user") + + userUUID, err := uuid.FromBytes(newUser.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, ts *httptest.Server) string { // Generate unique library name to avoid conflicts between tests