Skip to main content
Glama
test-docker.sh14.2 kB
#!/bin/bash # Docker Image Test Script for spec-workflow-mcp # Tests security configurations and basic functionality # # Usage: # ./test-docker.sh # Full build (requires ~4GB Docker memory) # ./test-docker.sh --prebuilt # Use pre-built files (run `npm run build` first) set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Expected test endpoint response (must match DASHBOARD_TEST_MESSAGE in src/dashboard/utils.ts) EXPECTED_TEST_MESSAGE="MCP Workflow Dashboard Online!" # Parse arguments USE_PREBUILT=false if [ "$1" = "--prebuilt" ]; then USE_PREBUILT=true fi # Test configuration IMAGE_NAME="spec-workflow-mcp-test" CONTAINER_NAME="spec-workflow-test" TEST_PORT=5050 # Cleanup function cleanup() { echo -e "\n${BLUE}Cleaning up...${NC}" docker stop "$CONTAINER_NAME" 2>/dev/null || true docker rm "$CONTAINER_NAME" 2>/dev/null || true } # Set trap to cleanup on exit trap cleanup EXIT # Helper functions log_test() { echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE}TEST: $1${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } log_pass() { echo -e "${GREEN}✓ PASS: $1${NC}" } log_fail() { echo -e "${RED}✗ FAIL: $1${NC}" } log_info() { echo -e "${YELLOW}ℹ INFO: $1${NC}" } wait_for_container() { local max_attempts=30 # 15 seconds max local attempt=0 while [ $attempt -lt $max_attempts ]; do # Wait for actual startup message or error - "Dashboard started at" indicates server is listening if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "Dashboard started at\|SECURITY ERROR"; then return 0 fi sleep 0.5 attempt=$((attempt + 1)) done return 1 } check_container_running() { docker ps --filter "name=$CONTAINER_NAME" --format "{{.Names}}" | grep -q "$CONTAINER_NAME" } # Function to test the /api/test endpoint test_api_endpoint() { local port=$1 local host=${2:-127.0.0.1} local max_attempts=${3:-10} # 10 retries with 1s sleep = 10 seconds max local attempt=1 # Brief wait after startup confirmation sleep 1 while [ $attempt -le $max_attempts ]; do response=$(curl -s --max-time 2 "http://${host}:${port}/api/test" 2>/dev/null || echo "") if echo "$response" | grep -q "$EXPECTED_TEST_MESSAGE"; then return 0 fi sleep 1 attempt=$((attempt + 1)) done return 1 } # Stop and remove test container stop_container() { docker stop "$CONTAINER_NAME" 2>/dev/null || true docker rm "$CONTAINER_NAME" 2>/dev/null || true } echo -e "${BLUE}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Spec-Workflow MCP Docker Test Suite ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${NC}" # ============================================================================ # TEST 1: Build Docker Image # ============================================================================ log_test "Building Docker Image" cd "$(dirname "$0")/.." if [ "$USE_PREBUILT" = true ]; then log_info "Using pre-built Dockerfile (Dockerfile.prebuilt)" # Check if dist directory exists if [ ! -d "dist" ]; then log_fail "dist/ directory not found. Run 'npm run build' first." exit 1 fi if docker build -f containers/Dockerfile.prebuilt -t "$IMAGE_NAME" . ; then log_pass "Docker image built successfully (using pre-built files)" else log_fail "Docker image build failed" exit 1 fi else log_info "Using full Dockerfile (requires ~4GB Docker memory)" if docker build -f containers/Dockerfile -t "$IMAGE_NAME" . ; then log_pass "Docker image built successfully" else log_fail "Docker image build failed" echo "" echo -e "${YELLOW}TIP: If build fails due to memory, try:${NC}" echo " 1. Increase Docker Desktop memory to 4GB+ in Settings > Resources" echo " 2. Or run: npm run build && ./test-docker.sh --prebuilt" exit 1 fi fi # ============================================================================ # TEST 2: Docker Default Configuration (0.0.0.0 binding, localhost exposure) # ============================================================================ log_test "Docker Default Configuration (app binds 0.0.0.0, Docker exposes to localhost)" stop_container # Default Docker behavior: app binds to 0.0.0.0, port exposed to host's localhost only # Note: Not mounting external volumes - container uses its own writable directories docker run -d \ --name "$CONTAINER_NAME" \ -p 127.0.0.1:$TEST_PORT:$TEST_PORT \ -e DASHBOARD_PORT=$TEST_PORT \ "$IMAGE_NAME" if wait_for_container; then if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "Dashboard started at\|Bind Address: 0.0.0.0"; then log_pass "Dashboard started with Docker default configuration" else log_fail "Dashboard did not start properly" docker logs "$CONTAINER_NAME" fi # Verify dashboard is responding via /api/test endpoint if test_api_endpoint $TEST_PORT 127.0.0.1; then log_pass "Dashboard /api/test endpoint responding correctly" else log_info "Dashboard /api/test endpoint not responding (may need more startup time)" fi else log_fail "Container startup timed out" docker logs "$CONTAINER_NAME" fi # Show security config from logs log_info "Security configuration:" docker logs "$CONTAINER_NAME" 2>&1 | grep -A6 "Security Configuration:" || true # ============================================================================ # TEST 3: Security Check Still Works (override to test non-Docker behavior) # ============================================================================ log_test "Security Check (external binding without opt-in should fail)" stop_container # Override Docker defaults to test the security check still works docker run -d \ --name "$CONTAINER_NAME" \ -e DASHBOARD_PORT=$TEST_PORT \ -e SPEC_WORKFLOW_BIND_ADDRESS=0.0.0.0 \ -e SPEC_WORKFLOW_ALLOW_EXTERNAL_ACCESS=false \ "$IMAGE_NAME" sleep 3 if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "SECURITY ERROR\|non-localhost"; then log_pass "Security check correctly blocks external binding without opt-in" else if check_container_running; then log_fail "Container should have failed to start, but it's running" docker logs "$CONTAINER_NAME" else # Check exit code exit_code=$(docker inspect "$CONTAINER_NAME" --format='{{.State.ExitCode}}') if [ "$exit_code" -ne 0 ]; then log_pass "Container exited with error (exit code: $exit_code)" else log_fail "Container exited but no security error detected" docker logs "$CONTAINER_NAME" fi fi fi # ============================================================================ # TEST 4: External Network Exposure (port mapping to all interfaces) # ============================================================================ log_test "External Network Exposure (port mapping to 0.0.0.0)" stop_container # This simulates exposing to the network (0.0.0.0 port mapping) docker run -d \ --name "$CONTAINER_NAME" \ -p $TEST_PORT:$TEST_PORT \ -e DASHBOARD_PORT=$TEST_PORT \ "$IMAGE_NAME" if wait_for_container; then if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "SECURITY WARNING"; then log_pass "Security warning displayed for external binding" else log_info "Security warning may not be displayed (check logs)" fi if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "Dashboard started at\|Bind Address: 0.0.0.0"; then log_pass "Dashboard started with external network exposure" else log_fail "Dashboard did not start properly" docker logs "$CONTAINER_NAME" fi # Verify dashboard is responding via /api/test endpoint if test_api_endpoint $TEST_PORT 127.0.0.1; then log_pass "Dashboard /api/test endpoint responding correctly" else log_info "Dashboard /api/test endpoint not responding (may need more startup time)" fi else log_fail "Container startup timed out" docker logs "$CONTAINER_NAME" fi # ============================================================================ # TEST 4b: Docker Compose DASHBOARD_HOST Variable # ============================================================================ log_test "Docker Compose DASHBOARD_HOST Variable" stop_container # Test that DASHBOARD_HOST env var works for network exposure SCRIPT_DIR="$(dirname "$0")" cd "$SCRIPT_DIR" DASHBOARD_HOST=0.0.0.0 DASHBOARD_PORT=$TEST_PORT docker-compose up -d 2>/dev/null || true sleep 3 # Check if the port is bound to 0.0.0.0 if docker-compose ps 2>/dev/null | grep -q "0.0.0.0:$TEST_PORT"; then log_pass "DASHBOARD_HOST=0.0.0.0 correctly exposes to all interfaces" else log_info "Port binding check skipped (docker-compose may not be available)" fi docker-compose down 2>/dev/null || true cd - >/dev/null # ============================================================================ # TEST 5: Rate Limiting Configuration # ============================================================================ log_test "Rate Limiting Configuration" # Check current container logs for rate limiting status if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "Rate Limiting: ENABLED"; then log_pass "Rate limiting is enabled by default" else log_info "Rate limiting status not found in logs (may be enabled)" fi # ============================================================================ # TEST 6: Container Runs as Non-Root User # ============================================================================ log_test "Container Security: Non-Root User" if check_container_running; then user=$(docker exec "$CONTAINER_NAME" whoami 2>/dev/null || echo "unknown") if [ "$user" = "node" ]; then log_pass "Container running as non-root user: $user" else log_fail "Container running as unexpected user: $user" fi else log_info "Container not running, skipping user check" fi # ============================================================================ # TEST 7: Rate Limiting Disabled Configuration # ============================================================================ log_test "Rate Limiting Disabled Configuration" stop_container docker run -d \ --name "$CONTAINER_NAME" \ -e DASHBOARD_PORT=$TEST_PORT \ -e SPEC_WORKFLOW_RATE_LIMIT_ENABLED=false \ "$IMAGE_NAME" if wait_for_container; then if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "Rate Limiting: DISABLED"; then log_pass "Rate limiting correctly disabled via environment variable" else log_info "Rate limiting status unclear (check logs)" docker logs "$CONTAINER_NAME" 2>&1 | grep -i "rate" || true fi else log_fail "Container startup timed out" fi # ============================================================================ # TEST 8: Custom Port Configuration # ============================================================================ log_test "Custom Port Configuration" stop_container CUSTOM_PORT=6060 docker run -d \ --name "$CONTAINER_NAME" \ -p 127.0.0.1:$CUSTOM_PORT:$CUSTOM_PORT \ -e DASHBOARD_PORT=$CUSTOM_PORT \ "$IMAGE_NAME" if wait_for_container; then if docker logs "$CONTAINER_NAME" 2>&1 | grep -q "$CUSTOM_PORT\|Dashboard started"; then log_pass "Dashboard started on custom port $CUSTOM_PORT" else log_info "Custom port configuration may have worked (check logs)" fi # Verify dashboard is responding on custom port via /api/test endpoint if test_api_endpoint $CUSTOM_PORT 127.0.0.1; then log_pass "Dashboard /api/test endpoint responding on port $CUSTOM_PORT" else log_info "Dashboard /api/test endpoint not responding on custom port" fi else log_fail "Container startup timed out" fi # ============================================================================ # Summary # ============================================================================ echo -e "\n${BLUE}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Test Summary ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo -e "${GREEN}Docker image tests completed!${NC}" echo "" echo "Docker security model:" echo " • App binds to 0.0.0.0 inside container (required for port forwarding)" echo " • Docker port mapping controls external exposure:" echo " - 127.0.0.1:5000:5000 → localhost only (default, secure)" echo " - 5000:5000 → all interfaces (network access)" echo "" echo "Key features validated:" echo " • Default Docker config works (localhost exposure)" echo " • Security check blocks unauthorized external binding" echo " • Rate limiting configurable via environment variable" echo " • Container runs as non-root user" echo "" echo -e "${YELLOW}Note: Use docker-compose.yml for secure defaults." echo -e "To expose to network, change port mapping from 127.0.0.1:5000:5000 to 5000:5000${NC}"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Pimzino/spec-workflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server