diff --git a/cmd/server/tests/filters_test.go b/cmd/server/tests/filters_test.go new file mode 100644 index 0000000..6449cf1 --- /dev/null +++ b/cmd/server/tests/filters_test.go @@ -0,0 +1,216 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSavedFilters(t *testing.T) { + setup := setupTestServer(t) + client := &http.Client{} + + t.Run("GET /api/saved-filters without auth returns 401", func(t *testing.T) { + httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters?resource_type=media-items", nil) + resp, err := client.Do(httpReq) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("GET /api/saved-filters with user auth returns empty array initially", func(t *testing.T) { + httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters?resource_type=media-items", nil) + httpReq.Header.Set("Authorization", "Bearer "+setup.Token) + + resp, err := client.Do(httpReq) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var filters []map[string]interface{} + json.NewDecoder(resp.Body).Decode(&filters) + assert.Equal(t, 0, len(filters)) + }) + + t.Run("POST /api/saved-filters creates filter", func(t *testing.T) { + reqBody := map[string]interface{}{ + "name": "My Sci-Fi Books", + "resource_type": "media-items", + "filters": map[string]string{ + "genre_filter": "Science Fiction", + "sort": "title ASC", + }, + } + body, _ := json.Marshal(reqBody) + + httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body)) + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+setup.Token) + + resp, err := client.Do(httpReq) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var filter map[string]interface{} + json.NewDecoder(resp.Body).Decode(&filter) + assert.Equal(t, "My Sci-Fi Books", filter["name"]) + assert.Equal(t, "media-items", filter["resource_type"]) + assert.NotEmpty(t, filter["id"]) + // Filters field will be JSON object (JSONB as raw bytes) + assert.NotEmpty(t, filter["filters"]) + }) + + t.Run("POST /api/saved-filters with duplicate name returns 409", func(t *testing.T) { + reqBody := map[string]interface{}{ + "name": "Duplicate Test", + "resource_type": "media-items", + "filters": map[string]string{"search": "test"}, + } + body, _ := json.Marshal(reqBody) + + // First request + httpReq1, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body)) + httpReq1.Header.Set("Content-Type", "application/json") + httpReq1.Header.Set("Authorization", "Bearer "+setup.Token) + + resp1, err := client.Do(httpReq1) + require.NoError(t, err) + resp1.Body.Close() + + assert.Equal(t, http.StatusCreated, resp1.StatusCode) + + // Second request with same name + body2, _ := json.Marshal(reqBody) + httpReq2, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body2)) + httpReq2.Header.Set("Content-Type", "application/json") + httpReq2.Header.Set("Authorization", "Bearer "+setup.Token) + + resp2, err := client.Do(httpReq2) + require.NoError(t, err) + resp2.Body.Close() + + assert.Equal(t, http.StatusConflict, resp2.StatusCode) + }) + + t.Run("PUT /api/saved-filters/:id updates filter", func(t *testing.T) { + // Create filter first + createReq := map[string]interface{}{ + "name": "Original Name", + "resource_type": "media-items", + "filters": map[string]string{"search": "original"}, + } + body, _ := json.Marshal(createReq) + + createHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body)) + createHTTP.Header.Set("Content-Type", "application/json") + createHTTP.Header.Set("Authorization", "Bearer "+setup.Token) + + createResp, err := client.Do(createHTTP) + require.NoError(t, err) + defer createResp.Body.Close() + + assert.Equal(t, http.StatusCreated, createResp.StatusCode) + + var createdFilter map[string]interface{} + json.NewDecoder(createResp.Body).Decode(&createdFilter) + filterID := createdFilter["id"].(string) + + // Update filter + updateReq := map[string]interface{}{ + "name": "Updated Name", + "resource_type": "media-items", + "filters": map[string]string{"search": "updated"}, + } + updateBody, _ := json.Marshal(updateReq) + + updateHTTP, _ := http.NewRequest("PUT", setup.Server.URL+"/api/saved-filters/"+filterID, bytes.NewBuffer(updateBody)) + updateHTTP.Header.Set("Content-Type", "application/json") + updateHTTP.Header.Set("Authorization", "Bearer "+setup.Token) + + updateResp, err := client.Do(updateHTTP) + require.NoError(t, err) + defer updateResp.Body.Close() + + assert.Equal(t, http.StatusOK, updateResp.StatusCode) + + var updatedFilter map[string]interface{} + json.NewDecoder(updateResp.Body).Decode(&updatedFilter) + assert.Equal(t, "Updated Name", updatedFilter["name"]) + }) + + t.Run("DELETE /api/saved-filters/:id deletes filter", func(t *testing.T) { + // Create filter + createReq := map[string]interface{}{ + "name": "To Be Deleted", + "resource_type": "media-items", + "filters": map[string]string{}, + } + body, _ := json.Marshal(createReq) + + createHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body)) + createHTTP.Header.Set("Content-Type", "application/json") + createHTTP.Header.Set("Authorization", "Bearer "+setup.Token) + + createResp, err := client.Do(createHTTP) + require.NoError(t, err) + defer createResp.Body.Close() + + var createdFilter map[string]interface{} + json.NewDecoder(createResp.Body).Decode(&createdFilter) + filterID := createdFilter["id"].(string) + + // Delete filter + deleteHTTP, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/saved-filters/"+filterID, nil) + deleteHTTP.Header.Set("Authorization", "Bearer "+setup.Token) + + deleteResp, err := client.Do(deleteHTTP) + require.NoError(t, err) + deleteResp.Body.Close() + + assert.Equal(t, http.StatusNoContent, deleteResp.StatusCode) + }) + + t.Run("User cannot access another user's filter", func(t *testing.T) { + // Create regular user + user := createRegularUserOnce(t, setup.DB) + userToken := loginUserWithCredentials(t, setup.Server, user.Email, user.Password) + + // User creates a filter + createReq := map[string]interface{}{ + "name": "User1 Private", + "resource_type": "media-items", + "filters": map[string]string{}, + } + body, _ := json.Marshal(createReq) + + createHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body)) + createHTTP.Header.Set("Content-Type", "application/json") + createHTTP.Header.Set("Authorization", "Bearer "+userToken) + + createResp, err := client.Do(createHTTP) + require.NoError(t, err) + defer createResp.Body.Close() + + var createdFilter map[string]interface{} + json.NewDecoder(createResp.Body).Decode(&createdFilter) + filterID := createdFilter["id"].(string) + + // Admin user tries to delete regular user's filter + deleteHTTP, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/saved-filters/"+filterID, nil) + deleteHTTP.Header.Set("Authorization", "Bearer "+setup.Token) + + deleteResp, err := client.Do(deleteHTTP) + require.NoError(t, err) + deleteResp.Body.Close() + + assert.Equal(t, http.StatusNotFound, deleteResp.StatusCode) + }) +}