tubearr/scripts/docker-smoke-test.sh
John Lightner 4606dce553 feat: Tubearr — full project state through M006/S01
Migrated git root from W:/programming/Projects/ to W:/programming/Projects/Tubearr/.
Previous history preserved in Tubearr-full-backup.bundle at parent directory.

Completed milestones: M001 through M005
Active: M006/S02 (Add Channel UX)
2026-03-24 20:20:10 -05:00

239 lines
7.1 KiB
Bash

#!/usr/bin/env bash
# ============================================================
# Docker Smoke Test — Tubearr
#
# Builds the Docker image, starts a container, and verifies
# core endpoints work end-to-end. Tests restart persistence.
#
# Usage: bash scripts/docker-smoke-test.sh
# ============================================================
set -euo pipefail
# ── Configuration ──
IMAGE_NAME="tubearr"
# Container name must match docker-compose.yml container_name
CONTAINER_NAME="tubearr"
PORT=8989
HEALTH_TIMEOUT=90 # seconds to wait for healthy status
COMPOSE_FILE="docker-compose.yml"
# ── Color output helpers ──
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
pass() { echo -e "${GREEN}$1${NC}"; }
fail() { echo -e "${RED}$1${NC}"; }
info() { echo -e "${YELLOW}$1${NC}"; }
# ── Cleanup trap ──
cleanup() {
info "Cleaning up..."
docker-compose -f "$COMPOSE_FILE" down --volumes --remove-orphans 2>/dev/null || true
# Remove any leftover config/media dirs created by compose bind mounts
rm -rf ./config ./media 2>/dev/null || true
}
trap cleanup EXIT
# ── Pre-check: Ensure port is not already in use ──
info "Checking port $PORT availability"
if curl -sf "http://localhost:${PORT}/ping" >/dev/null 2>&1; then
fail "Port $PORT is already in use — another service is running. Stop it before running this test."
exit 1
fi
pass "Port $PORT is available"
# ── Step 1: Build Docker image ──
info "Building Docker image: $IMAGE_NAME"
docker build -t "$IMAGE_NAME" . || {
fail "Docker build failed"
exit 1
}
pass "Docker image built successfully"
# ── Step 2: Start container via docker-compose ──
info "Starting container via docker-compose"
docker-compose -f "$COMPOSE_FILE" up -d || {
fail "docker-compose up failed"
exit 1
}
pass "Container started"
# ── Step 3: Wait for healthy ──
info "Waiting for container to become healthy (timeout: ${HEALTH_TIMEOUT}s)"
elapsed=0
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
status=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
if [ "$status" = "healthy" ]; then
break
fi
if [ "$status" = "unhealthy" ]; then
fail "Container became unhealthy"
echo "Container logs:"
docker logs "$CONTAINER_NAME" 2>&1 | tail -30
exit 1
fi
sleep 2
elapsed=$((elapsed + 2))
done
if [ "$status" != "healthy" ]; then
fail "Container did not become healthy within ${HEALTH_TIMEOUT}s (status: $status)"
echo "Container logs:"
docker logs "$CONTAINER_NAME" 2>&1 | tail -30
exit 1
fi
pass "Container is healthy"
# ── Step 4: Verify /ping (unauthenticated) ──
info "Testing GET /ping"
PING_RESPONSE=$(curl -sf "http://localhost:${PORT}/ping" 2>&1) || {
fail "GET /ping failed"
exit 1
}
if echo "$PING_RESPONSE" | grep -q '"status":"ok"'; then
pass "GET /ping returns {\"status\":\"ok\"}"
else
fail "GET /ping unexpected response: $PING_RESPONSE"
exit 1
fi
# ── Step 5: Extract API key from container logs ──
info "Extracting API key from container logs"
# The auth plugin logs the generated key in a banner like:
# API Key generated (save this — it will not be shown again):
# <uuid>
# We look for a UUID-like string on a line by itself after the banner text.
# On restart with persisted state, the key won't be in logs — use TUBEARR_API_KEY env var as fallback.
API_KEY=$(docker logs "$CONTAINER_NAME" 2>&1 | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1 || true)
if [ -z "$API_KEY" ]; then
fail "Could not extract API key from container logs"
echo "Container logs:"
docker logs "$CONTAINER_NAME" 2>&1 | tail -20
exit 1
fi
pass "API key extracted: ${API_KEY:0:8}...${API_KEY: -4}"
# ── Step 6: Verify /api/v1/health (authenticated) ──
info "Testing GET /api/v1/health"
HEALTH_RESPONSE=$(curl -sf -H "X-Api-Key: $API_KEY" "http://localhost:${PORT}/api/v1/health" 2>&1) || {
fail "GET /api/v1/health failed"
exit 1
}
if echo "$HEALTH_RESPONSE" | grep -q '"status"'; then
pass "GET /api/v1/health returns health status"
else
fail "GET /api/v1/health unexpected response: $HEALTH_RESPONSE"
exit 1
fi
# ── Step 7: Verify /api/v1/system/status (authenticated) ──
info "Testing GET /api/v1/system/status"
STATUS_RESPONSE=$(curl -sf -H "X-Api-Key: $API_KEY" "http://localhost:${PORT}/api/v1/system/status" 2>&1) || {
fail "GET /api/v1/system/status failed"
exit 1
}
if echo "$STATUS_RESPONSE" | grep -q '"appName":"Tubearr"'; then
pass "GET /api/v1/system/status returns appName=Tubearr"
else
fail "GET /api/v1/system/status unexpected response: $STATUS_RESPONSE"
exit 1
fi
# ── Step 8: Verify auth rejection ──
info "Testing auth rejection (no API key)"
AUTH_CODE=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${PORT}/api/v1/system/status" 2>&1)
if [ "$AUTH_CODE" = "401" ]; then
pass "Unauthenticated request correctly returns 401"
else
fail "Expected 401, got $AUTH_CODE"
exit 1
fi
# ── Step 9: Test restart persistence ──
info "Testing container restart persistence"
# Record the API key before restart
PRE_RESTART_KEY="$API_KEY"
# Restart the container
docker-compose -f "$COMPOSE_FILE" restart || {
fail "docker-compose restart failed"
exit 1
}
# Wait for healthy again
info "Waiting for container to become healthy after restart"
elapsed=0
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
status=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
if [ "$status" = "healthy" ]; then
break
fi
if [ "$status" = "unhealthy" ]; then
fail "Container became unhealthy after restart"
docker logs "$CONTAINER_NAME" 2>&1 | tail -20
exit 1
fi
sleep 2
elapsed=$((elapsed + 2))
done
if [ "$status" != "healthy" ]; then
fail "Container did not become healthy after restart within ${HEALTH_TIMEOUT}s"
docker logs "$CONTAINER_NAME" 2>&1 | tail -20
exit 1
fi
pass "Container healthy after restart"
# Verify /ping still works
PING_AFTER=$(curl -sf "http://localhost:${PORT}/ping" 2>&1) || {
fail "GET /ping failed after restart"
exit 1
}
if echo "$PING_AFTER" | grep -q '"status":"ok"'; then
pass "GET /ping works after restart"
else
fail "GET /ping unexpected response after restart: $PING_AFTER"
exit 1
fi
# Verify the same API key works (state persisted via volume)
HEALTH_AFTER=$(curl -sf -H "X-Api-Key: $PRE_RESTART_KEY" "http://localhost:${PORT}/api/v1/health" 2>&1) || {
fail "GET /api/v1/health failed after restart with pre-restart API key"
exit 1
}
if echo "$HEALTH_AFTER" | grep -q '"status"'; then
pass "Pre-restart API key still works — state persisted"
else
fail "API key state not preserved across restart"
exit 1
fi
# ── Done ──
echo ""
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
echo -e "${GREEN} SMOKE TEST PASSED${NC}"
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
echo ""