feat: add GET /api/saved-filters/:id endpoint with comprehensive tests
Implement missing GET endpoint for retrieving individual saved filters by ID. This completes the CRUD API for saved filters and enables mobile/SPA clients to fetch filter details on-demand. Backend Implementation: - Add GetSavedFilterByID() handler method (internal/handlers/filters.go) - Parse filter ID from URL parameter - Validate UUID format, return 400 for invalid IDs - Call service layer for business logic + ownership verification - Return 404 if filter not found or doesn't belong to user - Return 200 with filter object including filters JSONB - Add GetSavedFilterByID() service method (internal/services/filters.go) - Call existing database query GetSavedFilterByID - Verify filter exists and belongs to user - Return descriptive error: "filter not found or access denied" - Reuses existing database query (no new SQL needed) - Register GET /:id route (internal/router/filters.go) - Add route before existing GET "" route - Follows RESTful routing conventions Integration Tests (cmd/server/tests/filters_test.go): - Test success case: Create filter, retrieve by ID, verify data - Test error case: Invalid UUID format returns 400 - Test error case: Non-existent filter returns 404 - Test error case: No authentication returns 401 - Test security case: Cross-user access returns 404 (not 403) - Admin creates filter, regular user tries to access - Uses setup.Token (admin) and setup.RegularToken - Verifies information leakage prevention API Design: - Endpoint: GET /api/saved-filters/:id - Authentication: JWT token required - Response format: SavedFilterResponse with filters as JSON - Error responses: 400 (invalid ID), 401 (no auth), 404 (not found) - Security: Returns 404 for cross-user access (hides existence) Benefits: - Completes CRUD API for saved filters - Enables future mobile/SPA clients - Follows existing handler/service/test patterns - Comprehensive security testing - No database changes required (reuses existing queries) Follows PROJECT_GUIDELINES.md service layer architecture and testing patterns.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -213,4 +214,104 @@ func TestSavedFilters(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, deleteResp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GET /api/saved-filters/:id retrieves single filter", func(t *testing.T) {
|
||||
// First create a filter
|
||||
reqBody := map[string]interface{}{
|
||||
"name": "Test Filter",
|
||||
"resource_type": "media-items",
|
||||
"filters": map[string]string{"search": "test"},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
createReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
createResp, err := client.Do(createReq)
|
||||
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)
|
||||
|
||||
// Now retrieve the filter by ID
|
||||
getReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
||||
getReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
getResp, err := client.Do(getReq)
|
||||
require.NoError(t, err)
|
||||
defer getResp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, getResp.StatusCode)
|
||||
|
||||
var retrievedFilter map[string]interface{}
|
||||
json.NewDecoder(getResp.Body).Decode(&retrievedFilter)
|
||||
assert.Equal(t, "Test Filter", retrievedFilter["name"])
|
||||
assert.Equal(t, "media-items", retrievedFilter["resource_type"])
|
||||
assert.Equal(t, filterID, retrievedFilter["id"])
|
||||
assert.NotEmpty(t, retrievedFilter["filters"])
|
||||
})
|
||||
|
||||
t.Run("GET /api/saved-filters/:id without auth returns 401", func(t *testing.T) {
|
||||
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/550e8400-e29b-41d4-a716-446655440000", 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/:id with invalid UUID returns 400", func(t *testing.T) {
|
||||
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/invalid-uuid", 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.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GET /api/saved-filters/:id with non-existent filter returns 404", func(t *testing.T) {
|
||||
fakeID := uuid.New().String()
|
||||
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+fakeID, 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.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GET /api/saved-filters/:id from different user returns 404", func(t *testing.T) {
|
||||
// Create filter with admin user (setup.Token)
|
||||
reqBody := map[string]interface{}{
|
||||
"name": "Admin Filter",
|
||||
"resource_type": "media-items",
|
||||
"filters": map[string]string{"search": "test"},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
createReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/saved-filters", bytes.NewBuffer(body))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
createResp, err := client.Do(createReq)
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
var createdFilter map[string]interface{}
|
||||
json.NewDecoder(createResp.Body).Decode(&createdFilter)
|
||||
filterID := createdFilter["id"].(string)
|
||||
// Try to access with regular user (setup.RegularToken)
|
||||
httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/api/saved-filters/"+filterID, nil)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+setup.RegularToken)
|
||||
getResp, err := client.Do(httpReq)
|
||||
require.NoError(t, err)
|
||||
defer getResp.Body.Close()
|
||||
// Should return 404 (not 403 - hide existence)
|
||||
assert.Equal(t, http.StatusNotFound, getResp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user