diff --git a/scripts/test-api.sh b/scripts/test-api.sh index aabf5f1..b1619ee 100644 --- a/scripts/test-api.sh +++ b/scripts/test-api.sh @@ -2,27 +2,27 @@ # ============================================================================= # Scrum4Me — API curl test script # ============================================================================= -# Usage: -# 1. Start the dev server: npm run dev -# 2. Log in as "lars" in the UI and create an API token (Settings → API Tokens) -# 3. Copy the token and set it below -# 4. Run: bash scripts/test-api.sh +# See scripts/README.md for full setup instructions. # -# Prerequisites: -# - Database seeded: npx prisma db seed -# - Dev server running on localhost:3000 -# - A valid API token for user "lars" -# - curl installed +# 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="" # Paste your API token here +TOKEN="" # API token for "lars" (full-permission user) +DEMO_TOKEN="" # API token for "demo" (read-only — used for 403 tests, optional) BASE_URL="http://localhost:3000" -# IDs — fill these in after running GET /api/products -PRODUCT_ID="" -SPRINT_ID="" -STORY_ID="" -TASK_ID="" +# IDs — see scripts/README.md for how to find these +PRODUCT_ID="" # A product owned by lars +SPRINT_ID="" # An active sprint in that product +STORY_ID="" # A story in that sprint (IN_SPRINT status) +TASK_ID="" # Any task in that story # ============================================================================= # Helpers @@ -35,7 +35,6 @@ check() { local label="$1" local expected="$2" local actual="$3" - if [ "$actual" = "$expected" ]; then echo " PASS $label" PASS=$((PASS + 1)) @@ -45,284 +44,319 @@ check() { 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 ──────────────────────────────────────────────────────────" } -auth_header() { - echo "Authorization: Bearer $TOKEN" +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 } # ============================================================================= -# TC-P-09 GET /api/products — happy path +# GET /api/products # ============================================================================= test_products() { header "GET /api/products" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "$(auth_header)" \ - "$BASE_URL/api/products") - check "TC-P-09 happy path (lars)" 200 "$status" + # TC-P-09 happy path + check "TC-P-09 happy path (lars)" 200 \ + "$(get "$BASE_URL/api/products")" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - "$BASE_URL/api/products") - check "TC-P-01 no token → 401" 401 "$status" + # TC-P-01 no token + check "TC-P-01 no token → 401" 401 \ + "$(no_auth_get "$BASE_URL/api/products")" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer invalid-token-xyz" \ - "$BASE_URL/api/products") - check "TC-P-02 invalid token → 401" 401 "$status" + # 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")" - # Capture product list and extract first product ID for downstream tests - response=$(curl -s -H "$(auth_header)" "$BASE_URL/api/products") - echo " Response: $response" | head -c 300 + # 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 "" } # ============================================================================= -# TC-NS-08 GET /api/products/:id/next-story — happy path +# GET /api/products/:id/next-story # ============================================================================= test_next_story() { header "GET /api/products/:id/next-story" + skip_if_empty "PRODUCT_ID" "$PRODUCT_ID" || return - if [ -z "$PRODUCT_ID" ]; then - echo " SKIP PRODUCT_ID not set — fill in scripts/test-api.sh" - return - fi + # 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")" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "$(auth_header)" \ - "$BASE_URL/api/products/$PRODUCT_ID/next-story") - check "TC-NS-08 happy path (lars)" "200 or 404" "$status" + # TC-NS-01 no token + check "TC-NS-01 no token → 401" 401 \ + "$(no_auth_get "$BASE_URL/api/products/$PRODUCT_ID/next-story")" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - "$BASE_URL/api/products/$PRODUCT_ID/next-story") - check "TC-NS-01 no token → 401" 401 "$status" + # 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")" } # ============================================================================= -# TC-ST-09 GET /api/sprints/:id/tasks — happy path +# GET /api/sprints/:id/tasks # ============================================================================= test_sprint_tasks() { header "GET /api/sprints/:id/tasks" + skip_if_empty "SPRINT_ID" "$SPRINT_ID" || return - if [ -z "$SPRINT_ID" ]; then - echo " SKIP SPRINT_ID not set — fill in scripts/test-api.sh" - return - fi + # 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")" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "$(auth_header)" \ - "$BASE_URL/api/sprints/$SPRINT_ID/tasks?limit=10") - check "TC-ST-09 happy path with limit=10" 200 "$status" + # TC-ST-06 custom limit + check "TC-ST-06 limit=3 → 200" 200 \ + "$(get "$BASE_URL/api/sprints/$SPRINT_ID/tasks?limit=3")" - status=$(curl -s -o /dev/null -w "%{http_code}" \ - "$BASE_URL/api/sprints/$SPRINT_ID/tasks") - check "TC-ST-01 no token → 401" 401 "$status" + # 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")" } # ============================================================================= -# TC-L-17–19 POST /api/stories/:id/log — all 3 log types +# 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 - if [ -z "$STORY_ID" ]; then - echo " SKIP STORY_ID not set — fill in scripts/test-api.sh" - 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 - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"type":"IMPLEMENTATION_PLAN","content":"Aanpak: stap 1 implementeer, stap 2 test."}' \ - "$BASE_URL/api/stories/$STORY_ID/log") - check "TC-L-17 IMPLEMENTATION_PLAN" 201 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"type":"TEST_RESULT","content":"Alle tests geslaagd.","status":"PASSED"}' \ - "$BASE_URL/api/stories/$STORY_ID/log") - check "TC-L-18 TEST_RESULT PASSED" 201 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"type":"COMMIT","content":"feat: implementatie afgerond","commit_hash":"abc1234","commit_message":"feat: ST-XXX implementatie"}' \ - "$BASE_URL/api/stories/$STORY_ID/log") - check "TC-L-19 COMMIT" 201 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"type":"UNKNOWN","content":"test"}' \ - "$BASE_URL/api/stories/$STORY_ID/log") - check "TC-L-07 invalid type → 400" 400 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"type":"IMPLEMENTATION_PLAN","content":"test"}' \ - "$BASE_URL/api/stories/$STORY_ID/log") - check "TC-L-01 no token → 401" 401 "$status" } # ============================================================================= -# TC-RO-10 PATCH /api/stories/:id/tasks/reorder — happy path +# 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 - if [ -z "$STORY_ID" ]; then - echo " SKIP STORY_ID not set — fill in scripts/test-api.sh" - 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 - - # Fetch current task IDs for the story first - task_ids=$(curl -s \ - -H "$(auth_header)" \ - "$BASE_URL/api/sprints/$SPRINT_ID/tasks?limit=10" \ - | grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//' | head -3 | tr '\n' ',' | sed 's/,$//') - - if [ -z "$task_ids" ]; then - echo " SKIP no tasks found for reorder test" - return - fi - - ids_json=$(echo "$task_ids" | awk -F',' '{for(i=1;i<=NF;i++) printf "\"%s\"%s", $i, (i /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 - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X PATCH \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"status":"IN_PROGRESS"}' \ - "$BASE_URL/api/tasks/$TASK_ID") - check "TC-T-12 status → IN_PROGRESS" 200 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X PATCH \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"status":"DONE"}' \ - "$BASE_URL/api/tasks/$TASK_ID") - check "TC-T-13 status → DONE" 200 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X PATCH \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"status":"INVALID"}' \ - "$BASE_URL/api/tasks/$TASK_ID") - check "TC-T-06 invalid status → 400" 400 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X PATCH \ - -H "Content-Type: application/json" \ - -d '{"status":"DONE"}' \ - "$BASE_URL/api/tasks/$TASK_ID") - check "TC-T-01 no token → 401" 401 "$status" } # ============================================================================= -# TC-TD-09–10 POST /api/todos — with and without product +# 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 - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"title":"Test todo zonder product"}' \ - "$BASE_URL/api/todos") - check "TC-TD-09 happy path without product_id" 201 "$status" + # 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")" - if [ -n "$PRODUCT_ID" ]; then - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d "{\"title\":\"Test todo met product\",\"product_id\":\"$PRODUCT_ID\"}" \ - "$BASE_URL/api/todos") - check "TC-TD-10 happy path with product_id" 201 "$status" + # 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 - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "$(auth_header)" \ - -H "Content-Type: application/json" \ - -d '{"title":""}' \ - "$BASE_URL/api/todos") - check "TC-TD-05 empty title → 400" 400 "$status" - - status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"title":"Test"}' \ - "$BASE_URL/api/todos") - check "TC-TD-01 no token → 401" 401 "$status" } # ============================================================================= -# Run all tests +# Entry point # ============================================================================= echo "============================================================" echo " Scrum4Me API Test Suite" -echo " Base URL: $BASE_URL" +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. Edit scripts/test-api.sh and add your API token." + echo "ERROR: TOKEN is not set. See scripts/README.md." exit 1 fi