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)
239 lines
7.1 KiB
Bash
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 ""
|