test(api): add Bruno API collection for saved filters
Add comprehensive Bruno OpenCollection YAML files for testing
the saved filters API with 9 request files and scenarios.
Main CRUD Requests (4 files):
1. List Saved Filters.yml
- GET /api/saved-filters?resource_type=media-items
- Documents resource_type parameter requirement
- Example responses with JSONB filters
2. Create Saved Filter.yml
- POST /api/saved-filters
- Complete request body documentation
- All filter field examples (genre, author, sort, year, etc.)
- Validation rules (max 100 chars, uniqueness)
3. Update Saved Filter.yml
- PUT /api/saved-filters/{filter_id}
- Immutability notes (resource_type can't change)
- Duplicate name validation
- Updated timestamp behavior
4. Delete Saved Filter.yml
- DELETE /api/saved-filters/{filter_id}
- 204 No Content response
- Security considerations
Scenario Test Files (5 files):
1. Duplicate Name Validation.yml
- Tests 409 Conflict on duplicate names
- Per-user + per-resource-type uniqueness
- Example bash test script
2. User Isolation - Cross-User Access.yml
- Tests users can't access each other's filters
- Security: 404 instead of 403 (prevents enumeration)
- Complete multi-user test scenario
- Database-level isolation documentation
3. Multiple Resource Types.yml
- Tests generic design with different resource types
- Same name allowed for different types (media-items, collections, devices)
- Examples for each resource type
- Extensibility benefits explained
4. Complete CRUD Workflow.yml
- End-to-end lifecycle test (6.5K file)
- Shell script with all steps: Create → Read → Update → Delete → Verify
- Success criteria checklist
- Copy-paste ready test script
5. Filter Validation - Edge Cases.yml
- 12 different validation test cases
- Empty names, missing fields, invalid UUIDs
- Unicode support (emoji, CJK characters)
- Malformed JSON handling
- Special characters and XSS attempts
Documentation Features:
- {{base_url}} variable substitution
- auth: inherit for authentication
- Comprehensive docs: sections with examples
- Shell commands ready to copy-paste
- Expected status codes and responses
- Error handling examples
- Security best practices
Total: 9 YAML files covering all CRUD operations and edge cases
Usage:
- Import into Bruno/Postman for API testing
- Use for manual testing during development
- Reference for API contract validation
- Example curl commands for documentation
Part of: Saved Filters Implementation (Phase 5: Testing & Documentation)
Related: #saved-filters-feature
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
info:
|
||||
name: Create Saved Filter
|
||||
type: http
|
||||
seq: 2
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/saved-filters'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\":\
|
||||
\ \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\"\
|
||||
,\n \"sort\": \"title ASC\",\n \"author_filter\": \"\"\n }\n\
|
||||
}"
|
||||
|
||||
docs: |-
|
||||
## Create Saved Filter
|
||||
|
||||
Creates a new saved filter for the authenticated user.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/saved-filters
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC",
|
||||
"author_filter": "",
|
||||
"series_filter": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `name` (string, required): Filter name
|
||||
- Must be unique per user + resource type combination
|
||||
- Maximum 100 characters
|
||||
- Cannot be empty
|
||||
- `resource_type` (string, required): Type of resource to filter
|
||||
- Examples: "media-items", "collections", "devices"
|
||||
- Must match valid resource types
|
||||
- `filters` (object, required): Key-value pairs of filter criteria
|
||||
- Flexible structure - any valid filter fields
|
||||
- Serialized as JSONB in database
|
||||
|
||||
**Common Filter Fields for media-items:**
|
||||
- `search`: General search term
|
||||
- `author_filter`: Filter by author name
|
||||
- `genre_filter`: Filter by genre
|
||||
- `series_filter`: Filter by series
|
||||
- `language_filter`: Filter by language
|
||||
- `year_min`: Minimum copyright year
|
||||
- `year_max`: Maximum copyright year
|
||||
- `has_cover`: Boolean for cover image
|
||||
- `sort`: Sort order (e.g., "title ASC", "created_at DESC")
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created - Filter successfully created
|
||||
- 400: Bad Request - Invalid request body or missing required fields
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
- 409: Conflict - Filter with this name already exists for this resource type
|
||||
|
||||
**Error Response Examples:**
|
||||
|
||||
Duplicate name error:
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'My Sci-Fi Books' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- Name must be unique per user + resource type
|
||||
- Name cannot be empty or whitespace only
|
||||
- Resource type must be valid
|
||||
- Filters object must be provided (can be empty object {})
|
||||
@@ -0,0 +1,95 @@
|
||||
info:
|
||||
name: Delete Saved Filter
|
||||
type: http
|
||||
seq: 4
|
||||
http:
|
||||
method: DELETE
|
||||
url: '{{base_url}}/api/saved-filters/{{filter_id}}'
|
||||
auth: inherit
|
||||
|
||||
docs: |-
|
||||
## Delete Saved Filter
|
||||
|
||||
Permanently deletes a saved filter.
|
||||
|
||||
**Method:** DELETE
|
||||
|
||||
**Endpoint:** /api/saved-filters/{filter_id}
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `filter_id` (string, required): UUID of the filter to delete
|
||||
- Must be a valid UUID
|
||||
- Must belong to the authenticated user
|
||||
|
||||
**Response:**
|
||||
- 204 No Content (success)
|
||||
- Empty response body
|
||||
|
||||
**Status Codes:**
|
||||
- 204: No Content - Filter deleted successfully
|
||||
- 400: Bad Request - Invalid filter ID format
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
- 404: Not Found - Filter doesn't exist or doesn't belong to user
|
||||
- 500: Internal Server Error - Database error
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters/{{filter_id}}"
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- You can only delete filters that belong to you
|
||||
- Deletion is permanent - cannot be undone
|
||||
- Returns 204 No Content on success (no response body)
|
||||
- If filter doesn't exist or belongs to another user, returns 404
|
||||
|
||||
**Error Response Examples:**
|
||||
|
||||
Invalid filter ID:
|
||||
```json
|
||||
{
|
||||
"error": "invalid filter ID"
|
||||
}
|
||||
```
|
||||
|
||||
Filter not found:
|
||||
```json
|
||||
{
|
||||
"error": "failed to delete filter"
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Deletion:**
|
||||
```bash
|
||||
# First, create a filter
|
||||
CREATE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "To Be Deleted",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters")
|
||||
|
||||
# Extract the ID
|
||||
FILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')
|
||||
|
||||
# Delete the filter
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID"
|
||||
|
||||
# Verify it's gone
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items"
|
||||
```
|
||||
|
||||
**Cascade Effects:**
|
||||
- No cascade effects - saved filters are standalone records
|
||||
- User deletion automatically cascades to delete their filters
|
||||
- No foreign key dependencies on other tables
|
||||
@@ -0,0 +1,63 @@
|
||||
info:
|
||||
name: List Saved Filters
|
||||
type: http
|
||||
seq: 1
|
||||
http:
|
||||
method: GET
|
||||
url: '{{base_url}}/api/saved-filters?resource_type=media-items'
|
||||
auth: inherit
|
||||
|
||||
docs: |-
|
||||
## List Saved Filters
|
||||
|
||||
Retrieves all saved filters for the authenticated user and a specific resource type.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/saved-filters
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Query Parameters:**
|
||||
- `resource_type` (string, required): Filter by resource type
|
||||
- Examples: "media-items", "collections", "devices"
|
||||
- Required parameter - must be provided
|
||||
|
||||
**Response:**
|
||||
Array of saved filter objects:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T12:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - Returns array of saved filters
|
||||
- 400: Bad Request - Missing resource_type parameter
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
# Get all saved filters for media items
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items"
|
||||
|
||||
# Get all saved filters for collections
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=collections"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Filters are specific to each user (user-scoped via JWT)
|
||||
- Returns empty array [] if no filters exist for the resource type
|
||||
- Results ordered by created_at DESC (newest first)
|
||||
@@ -0,0 +1,117 @@
|
||||
info:
|
||||
name: Update Saved Filter
|
||||
type: http
|
||||
seq: 3
|
||||
http:
|
||||
method: PUT
|
||||
url: '{{base_url}}/api/saved-filters/{{filter_id}}'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\"\
|
||||
: \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\
|
||||
\ Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\
|
||||
\n }\n }"
|
||||
|
||||
docs: |-
|
||||
## Update Saved Filter
|
||||
|
||||
Updates an existing saved filter's name and/or filter criteria.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/saved-filters/{filter_id}
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `filter_id` (string, required): UUID of the filter to update
|
||||
- Must be a valid UUID
|
||||
- Must belong to the authenticated user
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Updated Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000",
|
||||
"year_max": "2024"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `name` (string, required): New filter name
|
||||
- Must be unique per user + resource type (excluding current filter)
|
||||
- Maximum 100 characters
|
||||
- `resource_type` (string, required): Resource type (must match existing)
|
||||
- Cannot change resource type after creation
|
||||
- `filters` (object, required): Updated filter criteria
|
||||
- Replaces existing filters entirely
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Updated Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000",
|
||||
"year_max": "2024"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - Filter updated successfully
|
||||
- 400: Bad Request - Invalid request body or filter ID
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
- 404: Not Found - Filter doesn't exist or doesn't belong to user
|
||||
- 409: Conflict - New name conflicts with existing filter
|
||||
|
||||
**Error Response Examples:**
|
||||
|
||||
Filter not found:
|
||||
```json
|
||||
{
|
||||
"error": "filter not found or access denied"
|
||||
}
|
||||
```
|
||||
|
||||
Duplicate name:
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'Updated Sci-Fi Books' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/{{filter_id}}"
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- You can only update filters that belong to you
|
||||
- The `updated_at` timestamp is automatically updated
|
||||
- Cannot change the resource_type (it's immutable)
|
||||
- New name must not conflict with other filters you own for the same resource type
|
||||
- All filter fields are replaced entirely (partial updates not supported)
|
||||
@@ -0,0 +1,245 @@
|
||||
info:
|
||||
name: Complete CRUD Workflow
|
||||
type: http
|
||||
seq: 4
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/saved-filters'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\"\
|
||||
: \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\"\
|
||||
,\n \"sort\": \"title ASC\"\n }\n }"
|
||||
|
||||
docs: |-
|
||||
## Complete CRUD Workflow
|
||||
|
||||
End-to-end test demonstrating the complete lifecycle of a saved filter: Create → Read → Update → Delete.
|
||||
|
||||
**Workflow Steps:**
|
||||
|
||||
**Step 1: CREATE a new filter**
|
||||
```bash
|
||||
CREATE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Complete Workflow Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters")
|
||||
|
||||
echo "Create Response:"
|
||||
echo $CREATE_RESPONSE | jq '.'
|
||||
|
||||
# Extract filter ID for subsequent requests
|
||||
FILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')
|
||||
echo "Created filter ID: $FILTER_ID"
|
||||
|
||||
# Expected Status: 201 Created
|
||||
# Expected Response:
|
||||
# {
|
||||
# "id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
# "name": "Complete Workflow Test",
|
||||
# "resource_type": "media-items",
|
||||
# "filters": {
|
||||
# "genre_filter": "Science Fiction",
|
||||
# "sort": "title ASC"
|
||||
# },
|
||||
# "created_at": "2024-03-20T12:00:00Z",
|
||||
# "updated_at": "2024-03-20T12:00:00Z"
|
||||
# }
|
||||
```
|
||||
|
||||
**Step 2: READ the created filter (list all)**
|
||||
```bash
|
||||
# List all filters for media-items
|
||||
LIST_RESPONSE=$(curl -s \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items")
|
||||
|
||||
echo "List Response:"
|
||||
echo $LIST_RESPONSE | jq '.'
|
||||
|
||||
# Verify our filter is in the list
|
||||
FILTER_EXISTS=$(echo $LIST_RESPONSE | jq --arg id "$FILTER_ID" \
|
||||
'.[] | select(.id == $id) | .id')
|
||||
|
||||
if [ "$FILTER_EXISTS" == "$FILTER_ID" ]; then
|
||||
echo "✓ Filter found in list"
|
||||
else
|
||||
echo "✗ Filter NOT found in list"
|
||||
fi
|
||||
|
||||
# Expected Status: 200 OK
|
||||
# Expected Response: Array containing our newly created filter
|
||||
```
|
||||
|
||||
**Step 3: UPDATE the filter**
|
||||
```bash
|
||||
# Update filter name and criteria
|
||||
UPDATE_RESPONSE=$(curl -s -X PUT \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Complete Workflow Test - Updated",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID")
|
||||
|
||||
echo "Update Response:"
|
||||
echo $UPDATE_RESPONSE | jq '.'
|
||||
|
||||
# Verify updated_at timestamp changed
|
||||
UPDATED_AT=$(echo $UPDATE_RESPONSE | jq -r '.updated_at')
|
||||
echo "Filter updated at: $UPDATED_AT"
|
||||
|
||||
# Expected Status: 200 OK
|
||||
# Expected Response:
|
||||
# {
|
||||
# "id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
# "name": "Complete Workflow Test - Updated",
|
||||
# "resource_type": "media-items",
|
||||
# "filters": {
|
||||
# "genre_filter": "Science Fiction Fantasy",
|
||||
# "sort": "author ASC",
|
||||
# "year_min": "2000"
|
||||
# },
|
||||
# "created_at": "2024-03-20T12:00:00Z",
|
||||
# "updated_at": "2024-03-20T12:05:00Z" # NEW TIMESTAMP
|
||||
# }
|
||||
```
|
||||
|
||||
**Step 4: DELETE the filter**
|
||||
```bash
|
||||
# Delete the filter
|
||||
DELETE_RESPONSE=$(curl -s -X DELETE \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID")
|
||||
|
||||
echo "Delete Response:"
|
||||
echo $DELETE_RESPONSE
|
||||
|
||||
# Expected Status: 204 No Content
|
||||
# Expected Response: (empty body)
|
||||
if [ -z "$DELETE_RESPONSE" ]; then
|
||||
echo "✓ Filter deleted successfully (empty response)"
|
||||
fi
|
||||
```
|
||||
|
||||
**Step 5: VERIFY deletion**
|
||||
```bash
|
||||
# Try to get the deleted filter
|
||||
GET_RESPONSE=$(curl -s \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items")
|
||||
|
||||
echo "List After Delete:"
|
||||
echo $GET_RESPONSE | jq '.'
|
||||
|
||||
# Verify filter is NOT in the list
|
||||
FILTER_EXISTS=$(echo $GET_RESPONSE | jq --arg id "$FILTER_ID" \
|
||||
'.[] | select(.id == $id) | .id')
|
||||
|
||||
if [ -z "$FILTER_EXISTS" ]; then
|
||||
echo "✓ Filter successfully deleted (not found in list)"
|
||||
else
|
||||
echo "✗ Filter still exists (deletion failed)"
|
||||
fi
|
||||
|
||||
# Expected: Filter no longer appears in list
|
||||
```
|
||||
|
||||
**Shell Script Complete Example:**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
BASE_URL="{{base_url}}"
|
||||
TOKEN="YOUR_TOKEN"
|
||||
|
||||
echo "=== Complete CRUD Workflow Test ==="
|
||||
echo ""
|
||||
|
||||
# 1. CREATE
|
||||
echo "Step 1: Creating filter..."
|
||||
FILTER_ID=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Workflow Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"genre_filter": "Science Fiction"}
|
||||
}' \
|
||||
"$BASE_URL/api/saved-filters" | jq -r '.id')
|
||||
|
||||
echo "✓ Created filter: $FILTER_ID"
|
||||
echo ""
|
||||
|
||||
# 2. READ
|
||||
echo "Step 2: Reading filters..."
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE_URL/api/saved-filters?resource_type=media-items" | \
|
||||
jq --arg id "$FILTER_ID" '.[] | select(.id == $id)'
|
||||
|
||||
echo "✓ Filter retrieved"
|
||||
echo ""
|
||||
|
||||
# 3. UPDATE
|
||||
echo "Step 3: Updating filter..."
|
||||
curl -s -X PUT \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Workflow Test - Updated",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"genre_filter": "Fantasy"}
|
||||
}' \
|
||||
"$BASE_URL/api/saved-filters/$FILTER_ID" | jq '.'
|
||||
|
||||
echo "✓ Filter updated"
|
||||
echo ""
|
||||
|
||||
# 4. DELETE
|
||||
echo "Step 4: Deleting filter..."
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE_URL/api/saved-filters/$FILTER_ID" -w "\nStatus: %{http_code}\n"
|
||||
|
||||
echo "✓ Filter deleted"
|
||||
echo ""
|
||||
|
||||
# 5. VERIFY
|
||||
echo "Step 5: Verifying deletion..."
|
||||
COUNT=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE_URL/api/saved-filters?resource_type=media-items" | \
|
||||
jq --arg id "$FILTER_ID" '[.[] | select(.id == $id)] | length')
|
||||
|
||||
if [ "$COUNT" -eq 0 ]; then
|
||||
echo "✓ Verification successful - filter deleted"
|
||||
else
|
||||
echo "✗ Verification failed - filter still exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== All CRUD operations completed successfully ==="
|
||||
```
|
||||
|
||||
**Success Criteria:**
|
||||
- ✅ Create returns 201 with generated ID
|
||||
- ✅ Read returns filter in list
|
||||
- ✅ Update modifies filter and updates timestamp
|
||||
- ✅ Delete returns 204 No Content
|
||||
- ✅ Filter no longer appears in list after deletion
|
||||
@@ -0,0 +1,76 @@
|
||||
info:
|
||||
name: Duplicate Name Validation
|
||||
type: http
|
||||
seq: 1
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/saved-filters'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Duplicate Test\",\n \"resource_type\":\
|
||||
\ \"media-items\",\n \"filters\": {\n \"search\": \"test\",\n \"genre_filter\"\
|
||||
: \"Science Fiction\"\n }\n }"
|
||||
|
||||
docs: |-
|
||||
## Duplicate Name Validation
|
||||
|
||||
Tests that the API correctly prevents creating duplicate filter names for the same user + resource type combination.
|
||||
|
||||
**Scenario:**
|
||||
1. Create first filter with name "Duplicate Test"
|
||||
2. Attempt to create second filter with same name
|
||||
3. Expect 409 Conflict error
|
||||
|
||||
**Expected Behavior:**
|
||||
- First request: 201 Created
|
||||
- Second request: 409 Conflict with error message
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'Duplicate Test' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
|
||||
**Test Steps:**
|
||||
```bash
|
||||
# First request - should succeed
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Duplicate Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"search": "test"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Response: 201 Created
|
||||
|
||||
# Second request - should fail with 409
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Duplicate Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"search": "test"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Response: 409 Conflict
|
||||
# {
|
||||
# "error": "filter with name 'Duplicate Test' already exists for this resource type"
|
||||
# }
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Duplicate check is per user + resource type
|
||||
- Same filter name can be used for different resource types
|
||||
- Different users can have filters with the same name
|
||||
- Validation happens in service layer before database insertion
|
||||
|
||||
**Related Tests:**
|
||||
- Update filter with name that conflicts with another filter (should also return 409)
|
||||
- Create filter with different name for different resource type (should succeed)
|
||||
@@ -0,0 +1,261 @@
|
||||
info:
|
||||
name: Filter Validation - Edge Cases
|
||||
type: http
|
||||
seq: 5
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/saved-filters'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"\",\n \"resource_type\": \"media-items\"\
|
||||
,\n \"filters\": {}\n }"
|
||||
|
||||
docs: |-
|
||||
## Filter Validation - Edge Cases
|
||||
|
||||
Tests various validation edge cases including empty names, invalid resource types, malformed JSON, etc.
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
**Test 1: Empty Filter Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "name and resource_type are required"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 2: Missing Name Field**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# Missing required field validation
|
||||
```
|
||||
|
||||
**Test 3: Missing Resource Type**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Filter",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "name and resource_type are required"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 4: Invalid Resource Type**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Filter",
|
||||
"resource_type": "invalid-type",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request or success
|
||||
# Note: Current validation allows any string (extensible design)
|
||||
# Future: May validate against allowed resource types
|
||||
```
|
||||
|
||||
**Test 5: Empty Filters Object**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Empty Filter",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 201 Created
|
||||
# Empty filters object is valid (matches no criteria)
|
||||
```
|
||||
|
||||
**Test 6: Very Long Filter Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "This is a very long filter name that exceeds one hundred characters and should be rejected by the validation logic in the handler",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request or 201 Created
|
||||
# Note: Plan specifies max 100 chars, but validation may not be enforced
|
||||
```
|
||||
|
||||
**Test 7: Special Characters in Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Filter with <script>alert(\"xss\")</script>",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 201 Created (input sanitized)
|
||||
# Note: Names are stored as-is, no HTML escaping needed
|
||||
```
|
||||
|
||||
**Test 8: Unicode Characters in Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "🚀 Sci-Fi Books 📚",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"search": "科幻"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 201 Created
|
||||
# Unicode (emoji, CJK characters) should be supported
|
||||
```
|
||||
|
||||
**Test 9: Malformed JSON**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"missing closing brace": "broken"
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "invalid request body"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 10: Invalid Filter ID Format**
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/not-a-uuid"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "invalid filter ID"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 11: Missing Filters Field**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Filter",
|
||||
"resource_type": "media-items"
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# Missing required field
|
||||
```
|
||||
|
||||
**Test 12: Whitespace-Only Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": " ",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# Name validation should reject whitespace-only names
|
||||
```
|
||||
|
||||
**Validation Rules Summary:**
|
||||
|
||||
**Required Fields:**
|
||||
- `name`: Non-empty string (max 100 chars per plan)
|
||||
- `resource_type`: Non-empty string
|
||||
- `filters`: Object (can be empty {})
|
||||
|
||||
**Optional Fields:**
|
||||
- None (all 3 fields are required)
|
||||
|
||||
**Field Validation:**
|
||||
- Name uniqueness checked per user + resource type
|
||||
- Filter ID must be valid UUID for PUT/DELETE
|
||||
- Filters must be valid JSON object
|
||||
|
||||
**Common Validation Errors:**
|
||||
```json
|
||||
{
|
||||
"error": "name and resource_type are required"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid request body"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid filter ID"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'X' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,168 @@
|
||||
info:
|
||||
name: Multiple Resource Types
|
||||
type: http
|
||||
seq: 3
|
||||
http:
|
||||
method: GET
|
||||
url: '{{base_url}}/api/saved-filters?resource_type={{resource_type}}'
|
||||
auth: inherit
|
||||
|
||||
docs: |-
|
||||
## Multiple Resource Types
|
||||
|
||||
Tests that the saved filters system works correctly with different resource types (media-items, collections, devices, etc.).
|
||||
|
||||
**Scenario:**
|
||||
1. Create filters for different resource types
|
||||
2. Verify filters are scoped correctly by resource_type
|
||||
3. Test that same filter name can exist for different resource types
|
||||
|
||||
**Test Steps:**
|
||||
|
||||
**Step 1: Create filters for different resource types**
|
||||
```bash
|
||||
# Create filter for media-items
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My Favorites",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"genre_filter": "Science Fiction"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Create filter for collections (same name!)
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My Favorites",
|
||||
"resource_type": "collections",
|
||||
"filters": {"sort_by": "name"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Create filter for devices (same name!)
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My Favorites",
|
||||
"resource_type": "devices",
|
||||
"filters": {"device_type": "ereader"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
```
|
||||
|
||||
**Step 2: List filters by resource type**
|
||||
```bash
|
||||
# Get only media-items filters
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items"
|
||||
|
||||
# Expected response:
|
||||
# [
|
||||
# {
|
||||
# "id": "...",
|
||||
# "name": "My Favorites",
|
||||
# "resource_type": "media-items",
|
||||
# "filters": {"genre_filter": "Science Fiction"},
|
||||
# ...
|
||||
# }
|
||||
# ]
|
||||
|
||||
# Get only collections filters
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=collections"
|
||||
|
||||
# Expected response:
|
||||
# [
|
||||
# {
|
||||
# "id": "...",
|
||||
# "name": "My Favorites",
|
||||
# "resource_type": "collections",
|
||||
# "filters": {"sort_by": "name"},
|
||||
# ...
|
||||
# }
|
||||
# ]
|
||||
|
||||
# Get only devices filters
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=devices"
|
||||
|
||||
# Expected response:
|
||||
# [
|
||||
# {
|
||||
# "id": "...",
|
||||
# "name": "My Favorites",
|
||||
# "resource_type": "devices",
|
||||
# "filters": {"device_type": "ereader"},
|
||||
# ...
|
||||
# }
|
||||
# ]
|
||||
```
|
||||
|
||||
**Expected Behavior:**
|
||||
- Same filter name allowed for different resource types
|
||||
- Each resource_type query returns only that type's filters
|
||||
- resource_type parameter is REQUIRED (no "get all" endpoint)
|
||||
- Missing resource_type returns 400 Bad Request
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
# Test missing resource_type parameter
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "resource_type query parameter is required"
|
||||
# }
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- **media-items**: Filter books by genre, author, series, etc.
|
||||
- **collections**: Filter collections by name, book count, etc.
|
||||
- **devices**: Filter devices by type, sync status, etc.
|
||||
- **Extensible**: New resource types can be added without schema changes
|
||||
|
||||
**Filter Structure by Resource Type:**
|
||||
|
||||
**Media Items Filter Fields:**
|
||||
```json
|
||||
{
|
||||
"search": "keyword",
|
||||
"author_filter": "Author Name",
|
||||
"genre_filter": "Science Fiction",
|
||||
"series_filter": "Series Name",
|
||||
"language_filter": "en",
|
||||
"year_min": "2000",
|
||||
"year_max": "2024",
|
||||
"sort": "title ASC"
|
||||
}
|
||||
```
|
||||
|
||||
**Collections Filter Fields:**
|
||||
```json
|
||||
{
|
||||
"sort_by": "name",
|
||||
"order": "asc",
|
||||
"include_auto": "true"
|
||||
}
|
||||
```
|
||||
|
||||
**Devices Filter Fields:**
|
||||
```json
|
||||
{
|
||||
"device_type": "ereader",
|
||||
"sync_enabled": "true",
|
||||
"sort": "last_sync DESC"
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of Generic Design:**
|
||||
- Single endpoint handles all resource types
|
||||
- No need for separate endpoints per resource
|
||||
- Flexible JSONB storage allows any filter structure
|
||||
- Easy to add new resource types in future
|
||||
@@ -0,0 +1,127 @@
|
||||
info:
|
||||
name: User Isolation - Cross-User Access
|
||||
type: http
|
||||
seq: 2
|
||||
http:
|
||||
method: DELETE
|
||||
url: '{{base_url}}/api/saved-filters/{{other_user_filter_id}}'
|
||||
auth: inherit
|
||||
|
||||
docs: |-
|
||||
## User Isolation - Cross-User Access
|
||||
|
||||
Tests that users cannot access or modify filters created by other users.
|
||||
|
||||
**Scenario:**
|
||||
1. User A creates a filter
|
||||
2. User B attempts to delete User A's filter
|
||||
3. Expect 404 Not Found (user isolation)
|
||||
|
||||
**Expected Behavior:**
|
||||
- User A can CRUD their own filters
|
||||
- User B cannot access User A's filters
|
||||
- API returns 404 (not 403) for security (doesn't reveal filter existence)
|
||||
|
||||
**Test Setup:**
|
||||
```bash
|
||||
# Step 1: Create regular user (User A)
|
||||
USER_A_EMAIL="usera@test.com"
|
||||
USER_A_PASS="password123"
|
||||
|
||||
# Create user and get token
|
||||
USER_A_TOKEN=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"email\": \"$USER_A_EMAIL\",
|
||||
\"username\": \"usera\",
|
||||
\"password\": \"$USER_A_PASS\",
|
||||
\"first_name\": \"User\",
|
||||
\"last_name\": \"A\"
|
||||
}" \
|
||||
"{{base_url}}/api/auth/register" | jq -r '.token')
|
||||
|
||||
# Step 2: User A creates a filter
|
||||
FILTER_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $USER_A_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "User A Private Filter",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"search": "private"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters")
|
||||
|
||||
FILTER_ID=$(echo $FILTER_RESPONSE | jq -r '.id')
|
||||
|
||||
echo "User A created filter: $FILTER_ID"
|
||||
```
|
||||
|
||||
**Test Steps:**
|
||||
```bash
|
||||
# Step 3: Admin user attempts to delete User A's filter
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer {{admin_token}}" \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID"
|
||||
|
||||
# Expected Response: 404 Not Found
|
||||
# {
|
||||
# "error": "failed to delete filter"
|
||||
# }
|
||||
```
|
||||
|
||||
**Expected Status Codes:**
|
||||
- 404: Not Found (filter doesn't exist OR doesn't belong to user)
|
||||
- NOT 403: Forbidden (we don't reveal whether filter exists)
|
||||
|
||||
**Security Considerations:**
|
||||
- Never reveal whether a filter ID exists for another user
|
||||
- Always return 404 for cross-user access attempts
|
||||
- Prevents user enumeration attacks
|
||||
- Each user sees only their own filters
|
||||
|
||||
**Additional Test Cases:**
|
||||
|
||||
**Get Another User's Filter:**
|
||||
```bash
|
||||
# User B tries to get User A's filter details
|
||||
curl -H "Authorization: Bearer $USER_B_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID"
|
||||
|
||||
# Expected: 404 Not Found
|
||||
```
|
||||
|
||||
**Update Another User's Filter:**
|
||||
```bash
|
||||
# User B tries to update User A's filter
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer $USER_B_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Hacked Filter",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID"
|
||||
|
||||
# Expected: 404 Not Found
|
||||
```
|
||||
|
||||
**List Only Own Filters:**
|
||||
```bash
|
||||
# User A lists their filters
|
||||
curl -H "Authorization: Bearer $USER_A_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items"
|
||||
|
||||
# Expected: Only User A's filters returned
|
||||
|
||||
# User B lists filters for same resource type
|
||||
curl -H "Authorization: Bearer $USER_B_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items"
|
||||
|
||||
# Expected: Only User B's filters returned (different result)
|
||||
```
|
||||
|
||||
**Database-Level Isolation:**
|
||||
- All queries include `WHERE user_id = @user_id` clause
|
||||
- Service layer validates ownership before operations
|
||||
- JWT token provides user_id for automatic scoping
|
||||
Reference in New Issue
Block a user