#!/usr/bin/env bash # API smoke tests — chạy trên server đang lắng nghe. set -euo pipefail # shellcheck source=../lib/common.sh source "$(dirname "$0")/../lib/common.sh" BASE="${1:-http://127.0.0.1:${LM_TEST_PORT}}" MISSION_ID="${TEST_MISSION_ID:-}" RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' PASS=0 FAIL=0 log_pass() { echo -e "${GREEN}PASS${NC} $*"; PASS=$((PASS + 1)); } log_fail() { echo -e "${RED}FAIL${NC} $*"; FAIL=$((FAIL + 1)); } json_field() { python3 - "$1" "$2" <<'PY' import json, sys doc = json.loads(open(sys.argv[1]).read()) path = sys.argv[2].split(".") cur = doc for p in path: cur = cur[p] print(cur) PY } http_code() { curl -s -o "$1" -w '%{http_code}' "${CURL_OPTS[@]}" "${@:2}" } assert_code() { local name="$1" expect="$2" file="$3" shift 3 local code code="$(http_code "$file" "$@")" if [[ "$code" == "$expect" ]]; then log_pass "$name (HTTP $code)" else log_fail "$name — expected HTTP $expect, got $code" [[ -f "$file" ]] && head -c 400 "$file" >&2 || true echo >&2 fi } assert_json_true() { local name="$1" file="$2" expr="$3" if python3 - "$file" "$expr" <<'PY' import json, sys doc = json.loads(open(sys.argv[1]).read()) env = {"doc": doc, "any": any, "all": all, "len": len, "list": list, "isinstance": isinstance} ok = eval(sys.argv[2], {"__builtins__": {}}, env) sys.exit(0 if ok else 1) PY then log_pass "$name" else log_fail "$name" head -c 400 "$file" >&2 || true echo >&2 fi } TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT COOKIE_JAR="$TMP/cookies.txt" CURL_OPTS=() echo "API smoke tests → $BASE" echo # --- Health & static (no auth) --- assert_code "GET /api/health" 200 "$TMP/health.json" -X GET "$BASE/api/health" assert_json_true "health ok" "$TMP/health.json" 'doc.get("ok") is True' assert_code "GET /api/state without auth" 401 "$TMP/state_unauth.json" -X GET "$BASE/api/state" assert_code "POST /api/auth/login bad password" 401 "$TMP/login_bad.json" \ -X POST "$BASE/api/auth/login" \ -H 'Content-Type: application/json' \ -d '{"username":"Admin","password":"wrong"}' assert_code "POST /api/auth/login" 200 "$TMP/login.json" \ -X POST "$BASE/api/auth/login" \ -H 'Content-Type: application/json' \ -c "$COOKIE_JAR" \ -d '{"username":"Admin","password":"admin"}' assert_json_true "login user" "$TMP/login.json" 'doc.get("user",{}).get("username") == "Admin"' CURL_OPTS=(-b "$COOKIE_JAR" -c "$COOKIE_JAR") assert_code "GET /api/auth/me" 200 "$TMP/me.json" -X GET "$BASE/api/auth/me" assert_json_true "auth me" "$TMP/me.json" 'doc.get("user",{}).get("group_name") == "Administrators"' assert_code "GET /" 200 "$TMP/index.html" -X GET "$BASE/" assert_code "GET /auth.js" 200 "$TMP/auth.js" -X GET "$BASE/auth.js" assert_code "GET /missions.js" 200 "$TMP/missions.js" -X GET "$BASE/missions.js" assert_code "GET /topbar.js" 200 "$TMP/topbar.js" -X GET "$BASE/topbar.js" assert_code "GET /api/robot/status" 200 "$TMP/robot_status.json" -X GET "$BASE/api/robot/status" assert_json_true "robot status motion" "$TMP/robot_status.json" 'doc.get("motion") in ("paused", "running")' assert_json_true "robot status battery" "$TMP/robot_status.json" 'doc.get("battery_percent", 0) >= 0' assert_code "POST /api/robot/start" 200 "$TMP/robot_start.json" \ -X POST "$BASE/api/robot/start" -H 'Content-Type: application/json' -d '{}' assert_json_true "robot started" "$TMP/robot_start.json" 'doc.get("motion") == "running"' assert_code "POST /api/robot/pause" 200 "$TMP/robot_pause.json" \ -X POST "$BASE/api/robot/pause" -H 'Content-Type: application/json' -d '{}' assert_json_true "robot paused" "$TMP/robot_pause.json" 'doc.get("motion") == "paused"' assert_code "POST /api/robot/errors/reset" 200 "$TMP/robot_reset.json" \ -X POST "$BASE/api/robot/errors/reset" -H 'Content-Type: application/json' -d '{}' assert_json_true "robot health ok" "$TMP/robot_reset.json" 'doc.get("health") == "ok"' assert_code "PUT /api/auth/profile" 200 "$TMP/profile.json" \ -X PUT "$BASE/api/auth/profile" \ -H 'Content-Type: application/json' \ -d '{"display_name":"Admin Test"}' assert_json_true "profile display_name" "$TMP/profile.json" 'doc.get("user",{}).get("display_name") == "Admin Test"' assert_code "GET /api/state" 200 "$TMP/state.json" -X GET "$BASE/api/state" assert_code "GET /api/missions" 200 "$TMP/missions.json" -X GET "$BASE/api/missions" if [[ -z "$MISSION_ID" ]]; then MISSION_ID="$(python3 -c " import json doc = json.load(open('$TMP/missions.json')) missions = doc.get('missions', []) preferred = 'testmission00001' ids = [m.get('id') for m in missions if isinstance(m, dict)] if preferred in ids: print(preferred) elif ids: print(ids[0]) ")" fi if [[ -z "$MISSION_ID" ]]; then log_fail "no mission available for queue tests" exit 1 fi echo "Mission id: $MISSION_ID" echo assert_json_true "missions available" "$TMP/missions.json" 'len(doc.get("missions", [])) >= 1' # --- Queue pause/continue (chạy sớm, trước các test enqueue khác) --- curl -s "${CURL_OPTS[@]}" -X DELETE "$BASE/api/mission_queue" -o /dev/null || true curl -s "${CURL_OPTS[@]}" -X POST "$BASE/api/mission_queue" \ -H 'Content-Type: application/json' \ -d "{\"mission_id\":\"$MISSION_ID\"}" -o "$TMP/qpost.json" for _ in $(seq 1 30); do curl -s "${CURL_OPTS[@]}" "$BASE/api/mission_queue" -o "$TMP/runner_poll.json" RUNNER_STATE="$(python3 -c "import json; print(json.load(open('$TMP/runner_poll.json')).get('runner',{}).get('state',''))")" if [[ "$RUNNER_STATE" == "running" ]]; then break fi sleep 0.2 done if [[ "$RUNNER_STATE" == "running" ]]; then assert_code "POST /api/mission_queue/pause" 200 "$TMP/pause.json" \ -X POST "$BASE/api/mission_queue/pause" assert_json_true "runner paused" "$TMP/pause.json" 'doc.get("state") == "paused"' assert_code "POST /api/mission_queue/continue" 200 "$TMP/cont.json" \ -X POST "$BASE/api/mission_queue/continue" assert_json_true "runner not paused" "$TMP/cont.json" 'doc.get("state") != "paused"' for _ in $(seq 1 15); do curl -s "${CURL_OPTS[@]}" "$BASE/api/mission_queue" -o "$TMP/runner_poll.json" RUNNER_STATE="$(python3 -c "import json; print(json.load(open('$TMP/runner_poll.json')).get('runner',{}).get('state',''))")" if [[ "$RUNNER_STATE" == "running" || "$RUNNER_STATE" == "paused" ]]; then break fi sleep 0.2 done if [[ "$RUNNER_STATE" == "running" || "$RUNNER_STATE" == "paused" ]]; then assert_code "POST /api/mission_queue/cancel" 200 "$TMP/cancel.json" \ -X POST "$BASE/api/mission_queue/cancel" for _ in $(seq 1 40); do curl -s "${CURL_OPTS[@]}" "$BASE/api/mission_queue" -o "$TMP/runner_poll.json" RUNNER_STATE="$(python3 -c "import json; print(json.load(open('$TMP/runner_poll.json')).get('runner',{}).get('state',''))")" if [[ "$RUNNER_STATE" == "idle" ]]; then break fi sleep 0.15 done assert_json_true "runner idle after cancel" "$TMP/runner_poll.json" \ 'doc.get("runner",{}).get("state") == "idle"' else log_fail "runner not active for cancel test (skipped)" fi else log_fail "runner never reached running (pause test skipped)" fi assert_code "POST /api/mission_queue/cancel idle" 400 "$TMP/cancel_idle.json" \ -X POST "$BASE/api/mission_queue/cancel" # --- LiDAR CRUD --- assert_code "POST /api/lidars" 201 "$TMP/lidar.json" \ -X POST "$BASE/api/lidars" \ -H 'Content-Type: application/json' \ -d '{"name":"smoke-lidar","ip":"192.168.99.1","port":2112}' LIDAR_ID="$(json_field "$TMP/lidar.json" id 2>/dev/null || echo "")" if [[ -n "$LIDAR_ID" ]]; then log_pass "lidar created id=$LIDAR_ID" PASS=$((PASS + 1)) assert_code "PUT /api/lidars" 200 "$TMP/lidar_put.json" \ -X PUT "$BASE/api/lidars/$LIDAR_ID" \ -H 'Content-Type: application/json' \ -d '{"name":"smoke-lidar","ip":"192.168.99.2","port":2112}' assert_code "DELETE /api/lidars" 204 "$TMP/lidar_del.txt" \ -X DELETE "$BASE/api/lidars/$LIDAR_ID" else log_fail "lidar create — no id in response" fi # --- IMU CRUD --- assert_code "POST /api/imus" 201 "$TMP/imu.json" \ -X POST "$BASE/api/imus" \ -H 'Content-Type: application/json' \ -d '{"name":"smoke-imu","frame_id":"imu_smoke","topic":"/imu/smoke","source":"external"}' IMU_ID="$(json_field "$TMP/imu.json" id 2>/dev/null || echo "")" if [[ -n "$IMU_ID" ]]; then log_pass "imu created id=$IMU_ID" PASS=$((PASS + 1)) assert_code "DELETE /api/imus" 204 "$TMP/imu_del.txt" -X DELETE "$BASE/api/imus/$IMU_ID" else log_fail "imu create — no id in response" fi # --- Clear queue --- curl -s "${CURL_OPTS[@]}" -X DELETE "$BASE/api/mission_queue" -o /dev/null || true curl -s "${CURL_OPTS[@]}" -X DELETE "$BASE/api/v2.0.0/mission_queue" -o /dev/null || true # --- MiR v2 enqueue (Cách C REST) --- assert_code "POST /api/v2.0.0/mission_queue" 201 "$TMP/v2q.json" \ -X POST "$BASE/api/v2.0.0/mission_queue" \ -H 'Content-Type: application/json' \ -d "{\"mission_id\":\"$MISSION_ID\",\"priority\":2,\"robot_id\":\"default\"}" assert_json_true "v2 queue entry mission_id" "$TMP/v2q.json" \ 'doc.get("mission_id") == "'"$MISSION_ID"'"' assert_code "GET /api/v2.0.0/mission_queue" 200 "$TMP/v2list.json" -X GET "$BASE/api/v2.0.0/mission_queue" for _ in $(seq 1 15); do curl -s "${CURL_OPTS[@]}" "$BASE/api/v2.0.0/mission_queue" -o "$TMP/v2list.json" if python3 -c "import json; d=json.load(open('$TMP/v2list.json')); exit(0 if isinstance(d,list) and len(d)>=1 else 1)" 2>/dev/null; then break fi sleep 0.2 done assert_json_true "v2 queue non-empty" "$TMP/v2list.json" 'isinstance(doc, list) and len(doc) >= 1' assert_code "GET /api/v2.0.0/status" 200 "$TMP/v2status.json" -X GET "$BASE/api/v2.0.0/status" assert_json_true "v2 status has state_text" "$TMP/v2status.json" '"state_text" in doc' # --- Modbus trigger (Cách C) --- assert_code "POST /api/triggers" 201 "$TMP/trig.json" \ -X POST "$BASE/api/triggers" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"smoke-trigger\",\"coil_id\":1001,\"mission_id\":\"$MISSION_ID\"}" TRIG_ID="$(json_field "$TMP/trig.json" id 2>/dev/null || echo "")" if [[ -n "$TRIG_ID" ]]; then log_pass "trigger created id=$TRIG_ID" PASS=$((PASS + 1)) assert_code "POST modbus fire coil" 200 "$TMP/fire.json" \ -X POST "$BASE/api/modbus/coils/1001/trigger" assert_code "DELETE /api/triggers" 204 "$TMP/trig_del.txt" \ -X DELETE "$BASE/api/triggers/$TRIG_ID" else log_fail "trigger create — no id" fi # --- Fleet schedule --- assert_code "POST /api/fleet/schedules" 201 "$TMP/sched.json" \ -X POST "$BASE/api/fleet/schedules" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"smoke-schedule\",\"mission_id\":\"$MISSION_ID\",\"start_mode\":\"asap\",\"priority\":1}" SCHED_ID="$(json_field "$TMP/sched.json" id 2>/dev/null || echo "")" if [[ -n "$SCHED_ID" ]]; then log_pass "schedule created id=$SCHED_ID" PASS=$((PASS + 1)) assert_code "DELETE /api/fleet/schedules" 204 "$TMP/sched_del.txt" \ -X DELETE "$BASE/api/fleet/schedules/$SCHED_ID" else log_fail "schedule create — no id" fi assert_code "GET /api/fleet/robots" 200 "$TMP/robots.json" -X GET "$BASE/api/fleet/robots" assert_json_true "robots list" "$TMP/robots.json" 'isinstance(doc, list) and len(doc) >= 1' echo echo "Results: $PASS passed, $FAIL failed" if [[ "$FAIL" -gt 0 ]]; then exit 1 fi