#!/usr/bin/env bash # ============================================================================= # Scrum4Me — API curl test script # ============================================================================= # See scripts/README.md for full setup instructions. # # Quick start: # 1. npx prisma db seed # 2. npm run dev # 3. Log in as "lars", go to Settings → API Tokens, create a token # 4. Paste it in TOKEN below # 5. Fill in the four IDs (see README for how to find them) # 6. Optionally: log in as "demo", create a token, paste in DEMO_TOKEN # 7. bash scripts/test-api.sh # ============================================================================= TOKEN="9c839ecd03590d2e37a331ca6701f79ada64ce0fef6942040e02723111952167" # API token for "lars" (full-permission user) DEMO_TOKEN="" # API token for "demo" (read-only — used for 403 tests, optional) BASE_URL="http://localhost:3001" # IDs — see scripts/README.md for how to find these PRODUCT_ID="cmoentdso0002541783aehb6j" # A product owned by lars SPRINT_ID="cmoepgsr7000a5417lygh1ggq" # An active sprint in that product STORY_ID="cmoepj1g8000c5417x29drkhy" # A story in that sprint (IN_SPRINT status) TASK_ID="cmod3elvd000004gtntw9u5gs" # Fill in after creating a task via Sprint Planning UI # ============================================================================= # Helpers # ============================================================================= PASS=0 FAIL=0 check() { local label="$1" local expected="$2" local actual="$3" if [ "$actual" = "$expected" ]; then echo " PASS $label" PASS=$((PASS + 1)) else echo " FAIL $label (expected HTTP $expected, got $actual)" FAIL=$((FAIL + 1)) fi } # Passes if actual matches any of the remaining arguments check_one_of() { local label="$1" local actual="$2" shift 2 for expected in "$@"; do if [ "$actual" = "$expected" ]; then echo " PASS $label" PASS=$((PASS + 1)) return fi done echo " FAIL $label (expected one of [$*], got $actual)" FAIL=$((FAIL + 1)) } header() { echo "" echo "── $1 ──────────────────────────────────────────────────────────" } get() { curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" "$@"; } post() { curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$1" "${@:2}"; } patch() { curl -s -o /dev/null -w "%{http_code}" -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$1" "${@:2}"; } get_demo() { curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $DEMO_TOKEN" "$@"; } post_demo() { curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer $DEMO_TOKEN" -H "Content-Type: application/json" -d "$1" "${@:2}"; } patch_demo() { curl -s -o /dev/null -w "%{http_code}" -X PATCH -H "Authorization: Bearer $DEMO_TOKEN" -H "Content-Type: application/json" -d "$1" "${@:2}"; } no_auth_get() { curl -s -o /dev/null -w "%{http_code}" "$@"; } no_auth_post() { curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -d "$1" "${@:2}"; } no_auth_patch() { curl -s -o /dev/null -w "%{http_code}" -X PATCH -H "Content-Type: application/json" -d "$1" "${@:2}"; } skip_if_empty() { local var_name="$1" local var_val="$2" if [ -z "$var_val" ]; then echo " SKIP $var_name not set — see scripts/README.md" return 1 fi return 0 } # ============================================================================= # GET /api/products # ============================================================================= test_products() { header "GET /api/products" # TC-P-09 happy path check "TC-P-09 happy path (lars)" 200 \ "$(get "$BASE_URL/api/products")" # TC-P-01 no token check "TC-P-01 no token → 401" 401 \ "$(no_auth_get "$BASE_URL/api/products")" # TC-P-02 invalid token check "TC-P-02 invalid token → 401" 401 \ "$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer invalid-token-xyz" "$BASE_URL/api/products")" # Print the product list so the user can copy IDs if needed echo "" echo " Product list:" curl -s -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products" | \ grep -o '"id":"[^"]*"\|"name":"[^"]*"' | paste - - | \ sed 's/"id":"//;s/","name":"/ → /;s/"//' | \ while read -r line; do echo " $line"; done echo "" } # ============================================================================= # GET /api/products/:id/next-story # ============================================================================= test_next_story() { header "GET /api/products/:id/next-story" skip_if_empty "PRODUCT_ID" "$PRODUCT_ID" || return # TC-NS-08 happy path — 200 if active sprint with stories, 404 if not check_one_of "TC-NS-08 happy path (lars, 200 or 404 both valid)" 200 404 \ "$(get "$BASE_URL/api/products/$PRODUCT_ID/next-story")" # TC-NS-01 no token check "TC-NS-01 no token → 401" 401 \ "$(no_auth_get "$BASE_URL/api/products/$PRODUCT_ID/next-story")" # TC-NS-02 invalid token check "TC-NS-02 invalid token → 401" 401 \ "$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer invalid-token" "$BASE_URL/api/products/$PRODUCT_ID/next-story")" # TC-NS-07 cross-user: unknown product check "TC-NS-07 unknown product id → 404" 404 \ "$(get "$BASE_URL/api/products/nonexistent-product-id/next-story")" } # ============================================================================= # GET /api/sprints/:id/tasks # ============================================================================= test_sprint_tasks() { header "GET /api/sprints/:id/tasks" skip_if_empty "SPRINT_ID" "$SPRINT_ID" || return # TC-ST-09 happy path with limit=10 check "TC-ST-09 happy path limit=10 → 200" 200 \ "$(get "$BASE_URL/api/sprints/$SPRINT_ID/tasks?limit=10")" # TC-ST-06 custom limit check "TC-ST-06 limit=3 → 200" 200 \ "$(get "$BASE_URL/api/sprints/$SPRINT_ID/tasks?limit=3")" # TC-ST-01 no token check "TC-ST-01 no token → 401" 401 \ "$(no_auth_get "$BASE_URL/api/sprints/$SPRINT_ID/tasks")" # TC-ST-03 unknown sprint check "TC-ST-03 unknown sprint id → 404" 404 \ "$(get "$BASE_URL/api/sprints/nonexistent-sprint-id/tasks")" } # ============================================================================= # POST /api/stories/:id/log (all 3 log types) # ============================================================================= test_story_log() { header "POST /api/stories/:id/log" skip_if_empty "STORY_ID" "$STORY_ID" || return # TC-L-17 IMPLEMENTATION_PLAN check "TC-L-17 IMPLEMENTATION_PLAN → 201" 201 \ "$(post '{"type":"IMPLEMENTATION_PLAN","content":"Aanpak: stap 1 implementeer, stap 2 test."}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-18 TEST_RESULT PASSED check "TC-L-18 TEST_RESULT PASSED → 201" 201 \ "$(post '{"type":"TEST_RESULT","content":"Alle tests geslaagd.","status":"PASSED"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-19 COMMIT check "TC-L-19 COMMIT → 201" 201 \ "$(post '{"type":"COMMIT","content":"feat: implementatie afgerond","commit_hash":"abc1234","commit_message":"feat(ST-001): account aanmaken"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-07 invalid type check "TC-L-07 invalid type → 400" 400 \ "$(post '{"type":"UNKNOWN","content":"test"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-08 IMPLEMENTATION_PLAN missing content check "TC-L-08 missing content → 400" 400 \ "$(post '{"type":"IMPLEMENTATION_PLAN"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-10 TEST_RESULT missing status check "TC-L-10 TEST_RESULT missing status → 400" 400 \ "$(post '{"type":"TEST_RESULT","content":"done"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-01 no token check "TC-L-01 no token → 401" 401 \ "$(no_auth_post '{"type":"IMPLEMENTATION_PLAN","content":"test"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" # TC-L-03 demo user → 403 if [ -n "$DEMO_TOKEN" ]; then check "TC-L-03 demo user → 403" 403 \ "$(post_demo '{"type":"IMPLEMENTATION_PLAN","content":"test"}' \ "$BASE_URL/api/stories/$STORY_ID/log")" else echo " SKIP TC-L-03 demo token not set (set DEMO_TOKEN to enable)" fi } # ============================================================================= # PATCH /api/stories/:id/tasks/reorder # ============================================================================= test_reorder() { header "PATCH /api/stories/:id/tasks/reorder" skip_if_empty "STORY_ID" "$STORY_ID" || return skip_if_empty "TASK_ID" "$TASK_ID" || return # TC-RO-10 happy path — single task is a valid reorder check "TC-RO-10 happy path → 200" 200 \ "$(patch "{\"task_ids\":[\"$TASK_ID\"]}" \ "$BASE_URL/api/stories/$STORY_ID/tasks/reorder")" # TC-RO-06 empty task_ids check "TC-RO-06 empty task_ids → 400" 400 \ "$(patch '{"task_ids":[]}' \ "$BASE_URL/api/stories/$STORY_ID/tasks/reorder")" # TC-RO-08 task ID not belonging to this story check "TC-RO-08 foreign task id → 400" 400 \ "$(patch '{"task_ids":["nonexistent-task-id-xyz"]}' \ "$BASE_URL/api/stories/$STORY_ID/tasks/reorder")" # TC-RO-01 no token check "TC-RO-01 no token → 401" 401 \ "$(no_auth_patch "{\"task_ids\":[\"$TASK_ID\"]}" \ "$BASE_URL/api/stories/$STORY_ID/tasks/reorder")" # TC-RO-03 demo user → 403 if [ -n "$DEMO_TOKEN" ]; then check "TC-RO-03 demo user → 403" 403 \ "$(patch_demo "{\"task_ids\":[\"$TASK_ID\"]}" \ "$BASE_URL/api/stories/$STORY_ID/tasks/reorder")" else echo " SKIP TC-RO-03 demo token not set (set DEMO_TOKEN to enable)" fi } # ============================================================================= # PATCH /api/tasks/:id # ============================================================================= test_tasks() { header "PATCH /api/tasks/:id" skip_if_empty "TASK_ID" "$TASK_ID" || return # TC-T-12 status → IN_PROGRESS check "TC-T-12 status → IN_PROGRESS" 200 \ "$(patch '{"status":"IN_PROGRESS"}' "$BASE_URL/api/tasks/$TASK_ID")" # TC-T-13 status → DONE check "TC-T-13 status → DONE" 200 \ "$(patch '{"status":"DONE"}' "$BASE_URL/api/tasks/$TASK_ID")" # Reset to TO_DO for repeatability patch '{"status":"TO_DO"}' "$BASE_URL/api/tasks/$TASK_ID" > /dev/null # TC-T-06 invalid status check "TC-T-06 invalid status → 400" 400 \ "$(patch '{"status":"INVALID"}' "$BASE_URL/api/tasks/$TASK_ID")" # TC-T-07 empty body check "TC-T-07 empty body → 400" 400 \ "$(patch '{}' "$BASE_URL/api/tasks/$TASK_ID")" # TC-T-01 no token check "TC-T-01 no token → 401" 401 \ "$(no_auth_patch '{"status":"DONE"}' "$BASE_URL/api/tasks/$TASK_ID")" # TC-T-03 demo user → 403 if [ -n "$DEMO_TOKEN" ]; then check "TC-T-03 demo user → 403" 403 \ "$(patch_demo '{"status":"DONE"}' "$BASE_URL/api/tasks/$TASK_ID")" else echo " SKIP TC-T-03 demo token not set (set DEMO_TOKEN to enable)" fi } # ============================================================================= # POST /api/todos # Note: product_id is REQUIRED by the API (z.string().min(1)). # TC-TD-09 ("without product_id") therefore returns 400, not 201. # ============================================================================= test_todos() { header "POST /api/todos" skip_if_empty "PRODUCT_ID" "$PRODUCT_ID" || return # TC-TD-10 happy path — product_id required check "TC-TD-10 happy path with product_id → 201" 201 \ "$(post "{\"title\":\"Test todo $(date +%s)\",\"product_id\":\"$PRODUCT_ID\"}" \ "$BASE_URL/api/todos")" # TC-TD-06 product_id missing → 400 (required by schema, not optional) check "TC-TD-06 missing product_id → 400" 400 \ "$(post '{"title":"Todo without product"}' \ "$BASE_URL/api/todos")" # TC-TD-04 title missing check "TC-TD-04 missing title → 400" 400 \ "$(post "{\"product_id\":\"$PRODUCT_ID\"}" \ "$BASE_URL/api/todos")" # TC-TD-05 empty title check "TC-TD-05 empty title → 400" 400 \ "$(post "{\"title\":\"\",\"product_id\":\"$PRODUCT_ID\"}" \ "$BASE_URL/api/todos")" # TC-TD-01 no token check "TC-TD-01 no token → 401" 401 \ "$(no_auth_post "{\"title\":\"Test\",\"product_id\":\"$PRODUCT_ID\"}" \ "$BASE_URL/api/todos")" # TC-TD-03 demo user → 403 if [ -n "$DEMO_TOKEN" ]; then check "TC-TD-03 demo user → 403" 403 \ "$(post_demo "{\"title\":\"Test\",\"product_id\":\"$PRODUCT_ID\"}" \ "$BASE_URL/api/todos")" else echo " SKIP TC-TD-03 demo token not set (set DEMO_TOKEN to enable)" fi } # ============================================================================= # Entry point # ============================================================================= echo "============================================================" echo " Scrum4Me API Test Suite" echo " Base URL : $BASE_URL" echo " Token : ${TOKEN:0:8}... (lars)" [ -n "$DEMO_TOKEN" ] && echo " Demo : ${DEMO_TOKEN:0:8}... (403 tests active)" echo "============================================================" if [ -z "$TOKEN" ]; then echo "" echo "ERROR: TOKEN is not set. See scripts/README.md." exit 1 fi test_products test_next_story test_sprint_tasks test_story_log test_reorder test_tasks test_todos echo "" echo "============================================================" echo " Results: $PASS passed, $FAIL failed" echo "============================================================" [ "$FAIL" -eq 0 ] && exit 0 || exit 1