fix(bruno): convert drifted body keys to body.data — CLI scenario runs now carry bodies

Follow-up to the sweep route audit, correcting its diagnosis: the CLI
DOES read .yml bodies — via OpenCollection's `body.data` string form
(the NewDevDBSetup script always worked because it used it). The
failing scenario POSTs used `body.json` (structured) / `body.jsonBody`
(string) — keys the CLI silently ignores, sending Content-Length: 0
with form-urlencoded content-type, so every server bind failed.

- 81 bodies converted to data-form (json maps serialized; jsonBody
  strings kept verbatim, {{vars}} intact).
- 6 pre-existing TRUNCATED payloads repaired: the original generator
  cut bodies at the first {{var}} (every SHA-256-identified scenario
  plus Sync Progress/Sync Bookmarks shipped mid-JSON and 400'd from
  day one). Rewritten whole, faithful to each file's documented
  purpose; book_sha256/book_uuid added to the environment.
- environments: opds_base_url correction from the sweep retained.

Verified live against the rebuilt dev container: the full koreader
scenario tree runs green through `bru run` — 14/14 (202s across
immediate/checkpoint/SHA-256 progress, 200s bookmarks, highlights,
notes; SHA-256 book identification exercised against a real
file_sha256).
This commit is contained in:
John O'Keefe committed 2026-10-17 12:22:35 -04:00
1 parent 63a20dcc95
commit 09b6693505
83 files changed
+386 -2769

No files matched your search

@@ -8,238 +8,11 @@ http:
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
data: "{\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\n\nEnd-to-end test demonstrating the complete lifecycle of a saved filter: Create → Read → Update → Delete.\n\n**Workflow Steps:**\n\n**Step 1: CREATE a new filter**\n```bash\nCREATE_RESPONSE=$(curl -s -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\")\n\necho \"Create Response:\"\necho $CREATE_RESPONSE | jq '.'\n\n# Extract filter ID for subsequent requests\nFILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')\necho \"Created filter ID: $FILTER_ID\"\n\n# Expected Status: 201 Created\n# Expected Response:\n# {\n# \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n# \"name\": \"Complete Workflow Test\",\n# \"resource_type\": \"media-items\",\n# \"filters\": {\n#\
\ \"genre_filter\": \"Science Fiction\",\n# \"sort\": \"title ASC\"\n# },\n# \"created_at\": \"2024-03-20T12:00:00Z\",\n# \"updated_at\": \"2024-03-20T12:00:00Z\"\n# }\n```\n\n**Step 2: READ the created filter (list all)**\n```bash\n# List all filters for media-items\nLIST_RESPONSE=$(curl -s \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters?resource_type=media-items\")\n\necho \"List Response:\"\necho $LIST_RESPONSE | jq '.'\n\n# Verify our filter is in the list\nFILTER_EXISTS=$(echo $LIST_RESPONSE | jq --arg id \"$FILTER_ID\" \\\n '.[] | select(.id == $id) | .id')\n\nif [ \"$FILTER_EXISTS\" == \"$FILTER_ID\" ]; then\n echo \"✓ Filter found in list\"\nelse\n echo \"✗ Filter NOT found in list\"\nfi\n\n# Expected Status: 200 OK\n# Expected Response: Array containing our newly created filter\n```\n\n**Step 3: UPDATE the filter**\n```bash\n# Update filter name and criteria\nUPDATE_RESPONSE=$(curl -s -X PUT \\\n -H \"Authorization: Bearer\
\ YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Complete Workflow Test - Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters/$FILTER_ID\")\n\necho \"Update Response:\"\necho $UPDATE_RESPONSE | jq '.'\n\n# Verify updated_at timestamp changed\nUPDATED_AT=$(echo $UPDATE_RESPONSE | jq -r '.updated_at')\necho \"Filter updated at: $UPDATED_AT\"\n\n# Expected Status: 200 OK\n# Expected Response:\n# {\n# \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n# \"name\": \"Complete Workflow Test - Updated\",\n# \"resource_type\": \"media-items\",\n# \"filters\": {\n# \"genre_filter\": \"Science Fiction Fantasy\",\n# \"sort\": \"author ASC\",\n# \"year_min\": \"2000\"\n# },\n# \"created_at\": \"2024-03-20T12:00:00Z\",\n# \"updated_at\": \"2024-03-20T12:05:00Z\"\
\ # NEW TIMESTAMP\n# }\n```\n\n**Step 4: DELETE the filter**\n```bash\n# Delete the filter\nDELETE_RESPONSE=$(curl -s -X DELETE \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters/$FILTER_ID\")\n\necho \"Delete Response:\"\necho $DELETE_RESPONSE\n\n# Expected Status: 204 No Content\n# Expected Response: (empty body)\nif [ -z \"$DELETE_RESPONSE\" ]; then\n echo \"✓ Filter deleted successfully (empty response)\"\nfi\n```\n\n**Step 5: VERIFY deletion**\n```bash\n# Try to get the deleted filter\nGET_RESPONSE=$(curl -s \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters?resource_type=media-items\")\n\necho \"List After Delete:\"\necho $GET_RESPONSE | jq '.'\n\n# Verify filter is NOT in the list\nFILTER_EXISTS=$(echo $GET_RESPONSE | jq --arg id \"$FILTER_ID\" \\\n '.[] | select(.id == $id) | .id')\n\nif [ -z \"$FILTER_EXISTS\" ]; then\n echo \"✓ Filter successfully deleted (not found in list)\"\nelse\n echo \"✗ Filter still\
\ exists (deletion failed)\"\nfi\n\n# Expected: Filter no longer appears in list\n```\n\n**Shell Script Complete Example:**\n```bash\n#!/bin/bash\n\nset -e # Exit on error\n\nBASE_URL=\"{{base_url}}\"\nTOKEN=\"YOUR_TOKEN\"\n\necho \"=== Complete CRUD Workflow Test ===\"\necho \"\"\n\n# 1. CREATE\necho \"Step 1: Creating filter...\"\nFILTER_ID=$(curl -s -X POST \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"genre_filter\": \"Science Fiction\"}\n }' \\\n \"$BASE_URL/api/saved-filters\" | jq -r '.id')\n\necho \"✓ Created filter: $FILTER_ID\"\necho \"\"\n\n# 2. READ\necho \"Step 2: Reading filters...\"\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters?resource_type=media-items\" | \\\n jq --arg id \"$FILTER_ID\" '.[] | select(.id == $id)'\n\necho \"✓ Filter retrieved\"\necho \"\"\n\n# 3. UPDATE\necho \"Step 3:\
\ Updating filter...\"\ncurl -s -X PUT \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Workflow Test - Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"genre_filter\": \"Fantasy\"}\n }' \\\n \"$BASE_URL/api/saved-filters/$FILTER_ID\" | jq '.'\n\necho \"✓ Filter updated\"\necho \"\"\n\n# 4. DELETE\necho \"Step 4: Deleting filter...\"\ncurl -s -X DELETE \\\n -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters/$FILTER_ID\" -w \"\\nStatus: %{http_code}\\n\"\n\necho \"✓ Filter deleted\"\necho \"\"\n\n# 5. VERIFY\necho \"Step 5: Verifying deletion...\"\nCOUNT=$(curl -s -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters?resource_type=media-items\" | \\\n jq --arg id \"$FILTER_ID\" '[.[] | select(.id == $id)] | length')\n\nif [ \"$COUNT\" -eq 0 ]; then\n echo \"✓ Verification successful - filter deleted\"\nelse\n echo \"✗ Verification failed - filter still exists\"\
\n exit 1\nfi\n\necho \"\"\necho \"=== All CRUD operations completed successfully ===\"\n```\n\n**Success Criteria:**\n- ✅ Create returns 201 with generated ID\n- ✅ Read returns filter in list\n- ✅ Update modifies filter and updates timestamp\n- ✅ Delete returns 204 No Content\n- ✅ Filter no longer appears in list after deletion"
@@ -8,69 +8,6 @@ http:
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)
data: "{\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\n\nTests that the API correctly prevents creating duplicate filter names for the same user + resource type combination.\n\n**Scenario:**\n1. Create first filter with name \"Duplicate Test\"\n2. Attempt to create second filter with same name\n3. Expect 409 Conflict error\n\n**Expected Behavior:**\n- First request: 201 Created\n- Second request: 409 Conflict with error message\n\n**Error Response:**\n```json\n{\n \"error\": \"filter with name 'Duplicate Test' already exists for this resource type\"\n}\n```\n\n**Test Steps:**\n```bash\n# First request - should succeed\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"search\": \"test\"}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Response: 201 Created\n\n# Second request - should fail with 409\ncurl -X POST \\\n -H \"Authorization: Bearer\
\ YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"search\": \"test\"}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Response: 409 Conflict\n# {\n# \"error\": \"filter with name 'Duplicate Test' already exists for this resource type\"\n# }\n```\n\n**Important Notes:**\n- Duplicate check is per user + resource type\n- Same filter name can be used for different resource types\n- Different users can have filters with the same name\n- Validation happens in service layer before database insertion\n\n**Related Tests:**\n- Update filter with name that conflicts with another filter (should also return 409)\n- Create filter with different name for different resource type (should succeed)"
@@ -8,254 +8,10 @@ http:
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"
}
```
data: "{\n \"name\": \"\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }"
docs: "## Filter Validation - Edge Cases\n\nTests various validation edge cases including empty names, invalid resource types, malformed JSON, etc.\n\n**Test Cases:**\n\n**Test 1: Empty Filter Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"name and resource_type are required\"\n# }\n```\n\n**Test 2: Missing Name Field**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Missing required field validation\n```\n\n**Test 3: Missing Resource Type**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\"\
\ \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"name and resource_type are required\"\n# }\n```\n\n**Test 4: Invalid Resource Type**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"resource_type\": \"invalid-type\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request or success\n# Note: Current validation allows any string (extensible design)\n# Future: May validate against allowed resource types\n```\n\n**Test 5: Empty Filters Object**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Empty Filter\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \
\ \"{{base_url}}/api/saved-filters\"\n\n# Expected: 201 Created\n# Empty filters object is valid (matches no criteria)\n```\n\n**Test 6: Very Long Filter Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"This is a very long filter name that exceeds one hundred characters and should be rejected by the validation logic in the handler\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request or 201 Created\n# Note: Plan specifies max 100 chars, but validation may not be enforced\n```\n\n**Test 7: Special Characters in Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Filter with <script>alert(\\\"xss\\\")</script>\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\
\n\n# Expected: 201 Created (input sanitized)\n# Note: Names are stored as-is, no HTML escaping needed\n```\n\n**Test 8: Unicode Characters in Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"\U0001F680 Sci-Fi Books \U0001F4DA\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"search\": \"科幻\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 201 Created\n# Unicode (emoji, CJK characters) should be supported\n```\n\n**Test 9: Malformed JSON**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n\"missing closing brace\": \"broken\"\n}' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"invalid request body\"\
\n# }\n```\n\n**Test 10: Invalid Filter ID Format**\n```bash\ncurl -X PUT \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters/not-a-uuid\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"invalid filter ID\"\n# }\n```\n\n**Test 11: Missing Filters Field**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"resource_type\": \"media-items\"\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Missing required field\n```\n\n**Test 12: Whitespace-Only Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \" \",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n\
\ }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Name validation should reject whitespace-only names\n```\n\n**Validation Rules Summary:**\n\n**Required Fields:**\n- `name`: Non-empty string (max 100 chars per plan)\n- `resource_type`: Non-empty string\n- `filters`: Object (can be empty {})\n\n**Optional Fields:**\n- None (all 3 fields are required)\n\n**Field Validation:**\n- Name uniqueness checked per user + resource type\n- Filter ID must be valid UUID for PUT/DELETE\n- Filters must be valid JSON object\n\n**Common Validation Errors:**\n```json\n{\n \"error\": \"name and resource_type are required\"\n}\n```\n\n```json\n{\n \"error\": \"invalid request body\"\n}\n```\n\n```json\n{\n \"error\": \"invalid filter ID\"\n}\n```\n\n```json\n{\n \"error\": \"filter with name 'X' already exists for this resource type\"\n}\n```"