diff --git a/cmd/server/tests/device_cap_test.go b/cmd/server/tests/device_cap_test.go index 0dbedf4..4fa118a 100644 --- a/cmd/server/tests/device_cap_test.go +++ b/cmd/server/tests/device_cap_test.go @@ -288,10 +288,13 @@ func TestListUsersIncludesMaxDevices(t *testing.T) { var users []map[string]interface{} json.NewDecoder(resp.Body).Decode(&users) - // Verify max_devices field is present in response + // Verify max_devices and device_count fields are present in response if len(users) > 0 { _, hasMaxDevices := users[0]["max_devices"] assert.True(t, hasMaxDevices, "max_devices field should be present in user list") + + _, hasDeviceCount := users[0]["device_count"] + assert.True(t, hasDeviceCount, "device_count field should be present in user list") } } diff --git a/cmd/server/tests/system_settings_test.go b/cmd/server/tests/system_settings_test.go new file mode 100644 index 0000000..d745b8a --- /dev/null +++ b/cmd/server/tests/system_settings_test.go @@ -0,0 +1,321 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestSystemSettingsHandler tests the system-wide scan settings endpoints +func TestSystemSettingsHandler(t *testing.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() + + 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) + }) + + t.Run("GET /api/libraries/scan-settings - Get settings as non-admin", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/libraries/scan-settings", nil) + req.Header.Set("Authorization", "Bearer valid-user-token") + rr := 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) + }) + + t.Run("GET /api/libraries/scan-settings - Get settings as admin", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/libraries/scan-settings", nil) + req.Header.Set("Authorization", "Bearer valid-admin-token") + rr := 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) + + 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"]) + }) + + t.Run("PUT /api/libraries/scan-settings - Update without auth", func(t *testing.T) { + payload := map[string]interface{}{ + "scan_frequency_minutes": 30, + "auto_scan_enabled": true, + } + jsonData, _ := json.Marshal(payload) + + req := httptest.NewRequest("PUT", "/api/libraries/scan-settings", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + rr := 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) + }) + + t.Run("PUT /api/libraries/scan-settings - Update as non-admin", func(t *testing.T) { + payload := map[string]interface{}{ + "scan_frequency_minutes": 30, + "auto_scan_enabled": true, + } + jsonData, _ := json.Marshal(payload) + + 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() + + 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) + }) + + t.Run("PUT /api/libraries/scan-settings - Update with invalid frequency", func(t *testing.T) { + testCases := []struct { + name string + scanFrequencyMinutes int + }{ + {"Frequency too low (14 minutes)", 14}, + {"Frequency too high (1441 minutes)", 1441}, + {"Frequency too low (0 minutes)", 0}, + {"Frequency negative (-10)", -10}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + payload := map[string]interface{}{ + "scan_frequency_minutes": tc.scanFrequencyMinutes, + "auto_scan_enabled": true, + } + jsonData, _ := json.Marshal(payload) + + 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() + + 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) + }) + } + }) + + t.Run("PUT /api/libraries/scan-settings - Update with missing required field", func(t *testing.T) { + payload := map[string]interface{}{ + "auto_scan_enabled": true, + } + jsonData, _ := json.Marshal(payload) + + 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() + + 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) + }) + + t.Run("PUT /api/libraries/scan-settings - Update with valid data", func(t *testing.T) { + testCases := []struct { + name string + scanFrequencyMinutes int + autoScanEnabled bool + }{ + {"Valid frequency (15 minutes)", 15, true}, + {"Valid frequency (60 minutes)", 60, true}, + {"Valid frequency (1440 minutes)", 1440, true}, + {"Valid frequency (120 minutes)", 120, false}, + {"Valid frequency (30 minutes)", 30, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + payload := map[string]interface{}{ + "scan_frequency_minutes": tc.scanFrequencyMinutes, + "auto_scan_enabled": tc.autoScanEnabled, + } + jsonData, _ := json.Marshal(payload) + + 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() + + 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) + + var response map[string]interface{} + err := json.NewDecoder(rr.Body).Decode(&response) + assert.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) { + 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() + + 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) + }) +} + +// TestSystemSettingsIntegration tests the integration of system settings with the scheduler +func TestSystemSettingsIntegration(t *testing.T) { + t.Run("System settings affect all libraries equally", func(t *testing.T) { + settings := map[string]interface{}{ + "scan_frequency_minutes": 60, + "auto_scan_enabled": true, + } + + assert.Equal(t, 60, settings["scan_frequency_minutes"]) + assert.Equal(t, true, settings["auto_scan_enabled"]) + }) + + t.Run("Disabling auto scan stops all library scans", func(t *testing.T) { + settings := map[string]interface{}{ + "scan_frequency_minutes": 60, + "auto_scan_enabled": false, + } + + 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) { + validFrequencies := []int{15, 30, 60, 120, 240, 480, 720, 1440} + invalidFrequencies := []int{0, 1, 14, 1441, 2000} + + for _, freq := range validFrequencies { + assert.True(t, freq >= 15 && freq <= 1440, "Frequency %d should be valid", freq) + } + + for _, freq := range invalidFrequencies { + assert.False(t, freq >= 15 && freq <= 1440, "Frequency %d should be invalid", freq) + } + }) +} diff --git a/cmd/server/tests/user_test.go b/cmd/server/tests/user_test.go index 6263497..ba206ce 100644 --- a/cmd/server/tests/user_test.go +++ b/cmd/server/tests/user_test.go @@ -485,79 +485,3 @@ func TestAdminOnlyEndpoints(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) }) } - -// TestScanSettings tests scan settings endpoints -func TestScanSettings(t *testing.T) { - t.Run("GET /api/library/scan-settings - Get settings without auth", func(t *testing.T) { - req := httptest.NewRequest("GET", "/api/library/scan-settings", nil) - rr := 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) - }) - - t.Run("PUT /api/library/scan-settings - Update with invalid frequency", func(t *testing.T) { - testCases := []struct { - name string - scanFrequencyMinutes int - }{ - {"Frequency too low (14 minutes)", 14}, - {"Frequency too high (1441 minutes)", 1441}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload := map[string]interface{}{ - "scan_frequency_minutes": tc.scanFrequencyMinutes, - "auto_scan_enabled": true, - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", "/api/library/scan-settings", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"scan_frequency_minutes must be between 15 and 1440"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusBadRequest, rr.Code) - }) - } - }) - - t.Run("PUT /api/library/scan-settings - Update with valid frequency", func(t *testing.T) { - payload := map[string]interface{}{ - "scan_frequency_minutes": 60, - "auto_scan_enabled": true, - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", "/api/library/scan-settings", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer valid-token") - rr := httptest.NewRecorder() - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"message":"scan settings updated successfully"}`)) - }) - - handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - }) -}