Skip to content

Instantly share code, notes, and snippets.

@dengjonathan
Created May 20, 2026 23:53
Show Gist options
  • Select an option

  • Save dengjonathan/441a804ffd64eef5f11e555644598658 to your computer and use it in GitHub Desktop.

Select an option

Save dengjonathan/441a804ffd64eef5f11e555644598658 to your computer and use it in GitHub Desktop.
folders curl test
#!/bin/bash
#
# Manual test script for folder-resource management RPCs (ENG-9743, ENG-9744).
#
# Exercises the four new endpoints added in PR #11524:
# POST /api/v1/folders/{folder_id}/resources (AddFolderResources)
# POST /api/v1/folders/{folder_id}/resources/remove (RemoveFolderResources)
# GET /api/v1/folders/{folder_id}/resources (ListFolderResources)
# GET /api/v1/folders/for-resource (ListFoldersForResource)
#
# Requires a running local backend and SIFT_API_KEY env var.
#
# Usage:
# export SIFT_API_KEY="your-api-key"
# ./test_folder_resources.sh
# Intentionally not using `set -e` / `pipefail`: a single failing curl or empty
# grep match would otherwise kill the script silently. The `check` function
# below reports each failure with context and the script keeps running.
set -u
BASE_URL="${SIFT_BASE_URL:-http://localhost:8080}"
AUTH="Authorization: Bearer ${SIFT_API_KEY:?Set SIFT_API_KEY}"
CT="Content-Type: application/json"
# Optional DB inspection between steps.
# Set PSQL to a command that opens a psql session against the local DB, e.g.:
# export PSQL='psql postgres://postgres:password@localhost:5432/azimuth'
# export PSQL='docker exec -i azimuth-postgres-1 psql -U postgres -d azimuth'
# Set INSPECT=1 to pause and run the folder-membership query after each step.
PSQL="${PSQL:-}"
INSPECT="${INSPECT:-0}"
pass=0
fail=0
db_inspect() {
[ "$INSPECT" = "1" ] || return 0
if [ -z "$PSQL" ]; then
echo " (skipping DB inspect: PSQL not set)"
return 0
fi
echo " --- sift:folder state ---"
$PSQL -v ON_ERROR_STOP=1 <<'SQL' || echo " (psql query failed)"
\pset format aligned
\pset border 2
SET app.is_admin = 'true';
SELECT
f.name AS folder_name,
f.folder_id,
emkv.entity_type AS resource_type,
emkv.entity_id AS resource_id,
emkv.created_date AS added_at
FROM entity_metadata_keys emk
JOIN entity_metadata_values emv
ON emv.entity_metadata_key_id = emk.entity_metadata_key_id
JOIN entity_metadata_key_values emkv
ON emkv.entity_metadata_key_id = emk.entity_metadata_key_id
AND emkv.entity_metadata_value_id = emv.entity_metadata_value_id
LEFT JOIN folders f
ON f.folder_id = emv.value_relation_resource_id
WHERE emk.name = 'sift:folder'
AND emk.is_system = true
AND emv.value_relation_resource_type = 'folder'
ORDER BY f.name NULLS LAST, emkv.entity_type, emkv.created_date;
SQL
}
pause() {
local label="${1:-step}"
echo ""
echo " >>> after: $label"
db_inspect
if [ "$INSPECT" = "1" ]; then
read -r -p " Press Enter to continue (Ctrl-C to stop)... " _
fi
}
check() {
local desc="$1" expected_code="$2" actual_code="$3" body="$4"
if [ "$actual_code" = "$expected_code" ]; then
echo "PASS: $desc (HTTP $actual_code)"
pass=$((pass + 1))
else
echo "FAIL: $desc (expected HTTP $expected_code, got HTTP ${actual_code:-<none>})"
if [ -z "$actual_code" ] || [ "$actual_code" = "000" ]; then
echo " Connection failure: backend at $BASE_URL did not return an HTTP response."
echo " Common causes: backend not running, crashed mid-request, or wrong port."
fi
echo " Response: ${body:-<empty>}"
fail=$((fail + 1))
fi
}
assert_contains() {
local desc="$1" body="$2" needle="$3"
if echo "$body" | grep -q "$needle"; then
echo "PASS: $desc"
pass=$((pass + 1))
else
echo "FAIL: $desc (missing '$needle')"
echo " Response: $body"
fail=$((fail + 1))
fi
}
assert_not_contains() {
local desc="$1" body="$2" needle="$3"
if echo "$body" | grep -q "$needle"; then
echo "FAIL: $desc (unexpected '$needle')"
echo " Response: $body"
fail=$((fail + 1))
else
echo "PASS: $desc"
pass=$((pass + 1))
fi
}
new_uuid() { uuidgen | tr '[:upper:]' '[:lower:]'; }
# Extract a JSON field by exact key. Tolerates camelCase and snake_case keys.
# Prints empty string and warns to stderr if the key is missing.
extract_id() {
local body="$1" camel="$2" snake="$3" label="$4"
local id
id=$(echo "$body" | grep -o "\"${camel}\":\"[^\"]*\"" | head -1 | cut -d'"' -f4)
if [ -z "$id" ]; then
id=$(echo "$body" | grep -o "\"${snake}\":\"[^\"]*\"" | head -1 | cut -d'"' -f4)
fi
if [ -z "$id" ]; then
echo " WARN: could not extract ${label} from response body:" >&2
echo " ${body:-<empty>}" >&2
fi
echo "$id"
}
# --- Setup: create two folders ---
echo "=== Setup: create source + destination folders ==="
SOURCE_FOLDER_NAME="curl-src-$(date +%s)"
DEST_FOLDER_NAME="curl-dst-$(date +%s)"
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders" \
-H "$AUTH" -H "$CT" \
-d "{\"name\":\"$SOURCE_FOLDER_NAME\",\"description\":\"src for resource tests\"}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Create source folder" "200" "$CODE" "$BODY"
SOURCE_FOLDER_ID=$(extract_id "$BODY" folderId folder_id SOURCE_FOLDER_ID)
echo " SOURCE_FOLDER_ID=${SOURCE_FOLDER_ID:-<missing>}"
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders" \
-H "$AUTH" -H "$CT" \
-d "{\"name\":\"$DEST_FOLDER_NAME\",\"description\":\"dst for resource tests\"}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Create destination folder" "200" "$CODE" "$BODY"
DEST_FOLDER_ID=$(extract_id "$BODY" folderId folder_id DEST_FOLDER_ID)
echo " DEST_FOLDER_ID=${DEST_FOLDER_ID:-<missing>}"
pause "create source + destination folders"
# Synthetic resource ids — the API records folder membership by id, no FK check.
RULE_A=$(new_uuid)
RULE_B=$(new_uuid)
PANEL_A=$(new_uuid)
CALC_A=$(new_uuid)
echo " RULE_A=$RULE_A"
echo " RULE_B=$RULE_B"
echo " PANEL_A=$PANEL_A"
echo " CALC_A=$CALC_A"
# --- AddFolderResources ---
echo ""
echo "=== AddFolderResources ==="
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$SOURCE_FOLDER_ID\",
\"resources\":[
{\"resource_id\":\"$RULE_A\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_RULE\"},
{\"resource_id\":\"$RULE_B\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_RULE\"},
{\"resource_id\":\"$PANEL_A\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_PANEL_CONFIGURATION\"},
{\"resource_id\":\"$CALC_A\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_CALCULATED_CHANNEL\"}
]
}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Add 4 resources to source folder" "200" "$CODE" "$BODY"
assert_contains "All adds reported FOLDER_RESOURCE_STATUS_SUCCESS" "$BODY" "FOLDER_RESOURCE_STATUS_SUCCESS"
pause "add 4 resources to source folder"
# Idempotent re-add — duplicates should not error (ON CONFLICT DO NOTHING).
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$SOURCE_FOLDER_ID\",
\"resources\":[
{\"resource_id\":\"$RULE_A\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_RULE\"}
]
}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Re-add existing resource is idempotent" "200" "$CODE" "$BODY"
pause "idempotent re-add of RULE_A"
# --- ListFolderResources (must specify a resource_type) ---
echo ""
echo "=== ListFolderResources (resource_type=RULE) ==="
RESP=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources?resource_type=FOLDER_RESOURCE_TYPE_RULE" -H "$AUTH")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "List rules in source folder" "200" "$CODE" "$BODY"
assert_contains "Rule list returns RULE_A" "$BODY" "$RULE_A"
assert_contains "Rule list returns RULE_B" "$BODY" "$RULE_B"
assert_not_contains "Rule list excludes PANEL_A" "$BODY" "$PANEL_A"
assert_not_contains "Rule list excludes CALC_A" "$BODY" "$CALC_A"
echo ""
echo "=== ListFolderResources (resource_type=PANEL_CONFIGURATION) ==="
RESP=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources?resource_type=FOLDER_RESOURCE_TYPE_PANEL_CONFIGURATION" -H "$AUTH")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "List panels in source folder" "200" "$CODE" "$BODY"
assert_contains "Panel list returns PANEL_A" "$BODY" "$PANEL_A"
echo ""
echo "=== ListFolderResources without resource_type is rejected ==="
RESP=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources" -H "$AUTH")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Missing resource_type rejected" "400" "$CODE" "$BODY"
# --- ListFoldersForResource (reverse lookup) ---
echo ""
echo "=== ListFoldersForResource ($RULE_A) ==="
RESP=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/v1/folders/for-resource?resource_id=$RULE_A&resource_type=FOLDER_RESOURCE_TYPE_RULE" -H "$AUTH")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "List folders for RULE_A" "200" "$CODE" "$BODY"
assert_contains "Reverse lookup includes source folder" "$BODY" "$SOURCE_FOLDER_ID"
# --- RemoveFolderResources ---
echo ""
echo "=== RemoveFolderResources (drop PANEL_A) ==="
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources/remove" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$SOURCE_FOLDER_ID\",
\"resources\":[
{\"resource_id\":\"$PANEL_A\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_PANEL_CONFIGURATION\"}
]
}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Remove PANEL_A" "200" "$CODE" "$BODY"
RESP=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources?resource_type=FOLDER_RESOURCE_TYPE_PANEL_CONFIGURATION" -H "$AUTH")
BODY=$(echo "$RESP" | sed '$d')
assert_not_contains "PANEL_A no longer in source folder" "$BODY" "$PANEL_A"
pause "remove PANEL_A from source folder"
# Move RULE_B from source to dest via explicit remove + add (no migrate RPC).
echo ""
echo "=== Move RULE_B via Remove + Add ==="
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources/remove" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$SOURCE_FOLDER_ID\",
\"resources\":[{\"resource_id\":\"$RULE_B\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_RULE\"}]
}")
CODE=$(echo "$RESP" | tail -1)
check "Remove RULE_B from source" "200" "$CODE" "$(echo "$RESP" | sed '$d')"
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$DEST_FOLDER_ID/resources" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$DEST_FOLDER_ID\",
\"resources\":[{\"resource_id\":\"$RULE_B\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_RULE\"}]
}")
CODE=$(echo "$RESP" | tail -1)
check "Add RULE_B to destination" "200" "$CODE" "$(echo "$RESP" | sed '$d')"
SRC=$(curl -s "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources?resource_type=FOLDER_RESOURCE_TYPE_RULE" -H "$AUTH")
assert_not_contains "Source no longer has RULE_B" "$SRC" "$RULE_B"
assert_contains "Source still has RULE_A" "$SRC" "$RULE_A"
DST=$(curl -s "$BASE_URL/api/v1/folders/$DEST_FOLDER_ID/resources?resource_type=FOLDER_RESOURCE_TYPE_RULE" -H "$AUTH")
assert_contains "Destination has RULE_B" "$DST" "$RULE_B"
pause "move RULE_B from source to destination"
# --- Validation: invalid uuid ---
echo ""
echo "=== Validation ==="
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$SOURCE_FOLDER_ID\",
\"resources\":[
{\"resource_id\":\"not-a-uuid\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_RULE\"}
]
}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Invalid resource_id rejected" "400" "$CODE" "$BODY"
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/resources" \
-H "$AUTH" -H "$CT" \
-d "{
\"folder_id\":\"$SOURCE_FOLDER_ID\",
\"resources\":[
{\"resource_id\":\"$(new_uuid)\",\"resource_type\":\"FOLDER_RESOURCE_TYPE_UNSPECIFIED\"}
]
}")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "FOLDER_RESOURCE_TYPE_UNSPECIFIED rejected" "400" "$CODE" "$BODY"
RESP=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/v1/folders/$(new_uuid)/resources?resource_type=FOLDER_RESOURCE_TYPE_RULE" -H "$AUTH")
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "List on unknown folder returns 404" "404" "$CODE" "$BODY"
# --- Cleanup: archive both folders ---
echo ""
echo "=== Cleanup ==="
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$SOURCE_FOLDER_ID/archive" -H "$AUTH" -H "$CT" -d '{}')
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Archive source folder" "200" "$CODE" "$BODY"
RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/folders/$DEST_FOLDER_ID/archive" -H "$AUTH" -H "$CT" -d '{}')
BODY=$(echo "$RESP" | sed '$d')
CODE=$(echo "$RESP" | tail -1)
check "Archive destination folder" "200" "$CODE" "$BODY"
pause "archive both folders"
# --- Summary ---
echo ""
echo "=== Results: $pass passed, $fail failed ==="
[ "$fail" -eq 0 ] && exit 0 || exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment