Created
May 20, 2026 23:53
-
-
Save dengjonathan/441a804ffd64eef5f11e555644598658 to your computer and use it in GitHub Desktop.
folders curl test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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