Ctrl K
ComunidadGuías…

scripts/mobile-smoke.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Mobile runtime surface smoke tests.
# Launches the compiled codewhale-tui binary on loopback ports and verifies
# the mobile control page, auth, API routes, and binding behaviour through
# real HTTP requests.
#
# Usage:  ./scripts/mobile-smoke.sh
# Requires: curl, a built binary at target/release/codewhale
#           (the script will build it if cargo is available).

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BINARY="${BINARY:-${REPO_ROOT}/target/release/codewhale}"
PASS=0
FAIL=0
SERVER_PID=""

# ── helpers ──────────────────────────────────────────────────────────────────

log()  { printf "\033[1;34m>>> %s\033[0m\n" "$*"; }
pass() { printf "\033[1;32m  ✓ %s\033[0m\n" "$*"; PASS=$((PASS + 1)); }
fail() { printf "\033[1;31m  ✗ %s\033[0m\n" "$*"; FAIL=$((FAIL + 1)); }

cleanup() {
    if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
        kill "$SERVER_PID" 2>/dev/null || true
        wait "$SERVER_PID" 2>/dev/null || true
    fi
}
trap cleanup EXIT

pick_port() {
    # Find a free TCP port on loopback.
    python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
}

start_server() {
    local port="$1"; shift
    log "Starting server on port $port: $*"
    "$BINARY" serve --port "$port" "$@" &
    SERVER_PID=$!
    # Wait for the server to become ready.
    for _ in $(seq 1 30); do
        if curl -sf --max-time 2 "http://127.0.0.1:${port}/health" >/dev/null 2>&1; then
            return 0
        fi
        sleep 0.3
    done
    fail "Server did not become ready on port $port"
    cleanup
    return 1
}

stop_server() {
    if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
        kill "$SERVER_PID" 2>/dev/null || true
        wait "$SERVER_PID" 2>/dev/null || true
        SERVER_PID=""
    fi
}

# assert_status METHOD PATH [HEADER_NAME:HEADER_VALUE] [JSON_BODY] EXPECTED_STATUS
assert_status() {
    local method="$1" path="$2" header="" body="" expected=""
    if [[ $# -eq 5 ]]; then
        header="$3"; body="$4"; expected="$5"
    elif [[ $# -eq 4 ]]; then
        header="$3"; expected="$4"
    else
        expected="$3"
    fi

    local url="http://127.0.0.1:${PORT}${path}"
    local curl_args=(-sf --max-time 10 -o /dev/null -w '%{http_code}' -X "$method")
    if [[ -n "$header" ]]; then
        curl_args+=(-H "$header")
    fi
    if [[ -n "$body" ]]; then
        curl_args+=(-H "Content-Type: application/json" --data "$body")
    fi

    local actual
    actual=$(curl "${curl_args[@]}" "$url" 2>/dev/null || true)

    if [[ "$actual" == "$expected" ]]; then
        pass "$method $path → $expected"
    else
        fail "$method $path → expected $expected, got $actual"
    fi
}

# assert_body_contains METHOD PATH HEADER BODY_SUBSTRING
assert_body_contains() {
    local method="$1" path="$2" header="$3" substring="$4"
    local url="http://127.0.0.1:${PORT}${path}"
    local curl_args=(-sf --max-time 10 -X "$method")
    if [[ -n "$header" ]]; then
        curl_args+=(-H "$header")
    fi

    local body
    body=$(curl "${curl_args[@]}" "$url" 2>/dev/null || true)

    if grep -q -- "$substring" <<< "$body"; then
        pass "$method $path body contains '$substring'"
    else
        fail "$method $path body missing '$substring'"
    fi
}

assert_body_not_contains() {
    local method="$1" path="$2" header="$3" substring="$4"
    local url="http://127.0.0.1:${PORT}${path}"
    local curl_args=(-sf --max-time 10 -X "$method")
    if [[ -n "$header" ]]; then
        curl_args+=(-H "$header")
    fi

    local body
    body=$(curl "${curl_args[@]}" "$url" 2>/dev/null || true)

    if grep -q -- "$substring" <<< "$body"; then
        fail "$method $path body unexpectedly contains '$substring'"
    else
        pass "$method $path body does not contain '$substring'"
    fi
}

# ── build ────────────────────────────────────────────────────────────────────

if [[ ! -x "$BINARY" ]]; then
    log "Binary not found; building codewhale-tui in release mode..."
    cargo build -p codewhale-cli --bin codewhale --release --locked
fi

log "Using binary: $BINARY"

# ── Test Group 1: Token auth ────────────────────────────────────────────────

TOKEN="smoke_test_token_$$"
PORT=$(pick_port)

log "=== Test Group 1: Token auth ==="
start_server "$PORT" --mobile --auth-token "$TOKEN"

assert_body_contains GET "/mobile" "" "Codewhale Mobile"
assert_body_not_contains GET "/mobile" "" "$TOKEN"
assert_status GET "/v1/threads/summary" 401
assert_status GET "/v1/threads/summary" "Authorization: Bearer ${TOKEN}" 200
assert_status POST "/v1/approvals/no_such_id" "Authorization: Bearer ${TOKEN}" '{"decision":"allow"}' 404

stop_server

# ── Test Group 2: Insecure mode ─────────────────────────────────────────────

PORT=$(pick_port)

log "=== Test Group 2: Insecure mode (no token) ==="
start_server "$PORT" --mobile --insecure

assert_body_contains GET "/mobile" "" "Codewhale Mobile"
assert_status GET "/v1/threads/summary" 200

stop_server

# ── Test Group 3: Non-loopback binding rejection ────────────────────────────

PORT=$(pick_port)

log "=== Test Group 3: Reject non-loopback mobile binding ==="
set +e
BIND_OUTPUT=$(python3 - "$BINARY" "$PORT" <<'PY'
import os
import signal
import subprocess
import sys

probe = subprocess.Popen(
    [sys.argv[1], "serve", "--host", "0.0.0.0", "--port", sys.argv[2], "--mobile", "--insecure"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    start_new_session=True,
)
try:
    output, _ = probe.communicate(timeout=10)
except subprocess.TimeoutExpired:
    try:
        os.killpg(probe.pid, signal.SIGKILL)
    except ProcessLookupError:
        pass
    output, _ = probe.communicate()
    sys.stdout.buffer.write(output)
    print("Non-loopback rejection probe timed out; terminated and reaped its process group.")
    sys.exit(124)
sys.stdout.buffer.write(output)
sys.exit(probe.returncode)
PY
)
BIND_STATUS=$?
set -e

if [[ "$BIND_STATUS" -eq 124 ]]; then
    fail "mobile did not reject a 0.0.0.0 binding within 10 seconds"
elif [[ "$BIND_STATUS" -ne 0 ]]; then
    pass "mobile rejects a 0.0.0.0 binding"
else
    fail "mobile unexpectedly accepted a 0.0.0.0 binding"
fi

if grep -qi -- "loopback-only" <<< "$BIND_OUTPUT"; then
    pass "rejection explains the loopback-only boundary"
else
    fail "rejection missing loopback-only guidance"
fi

# ── summary ──────────────────────────────────────────────────────────────────

echo ""
log "Results: $PASS passed, $FAIL failed"

if [[ "$FAIL" -gt 0 ]]; then
    exit 1
fi