ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
1#!/bin/bash
2# ═══════════════════════════════════════════════════════
3# ATlast Docker Stack Smoke Test
4# Validates the full stack is running and responding.
5# Usage: bash scripts/docker-smoke-test.sh
6# ═══════════════════════════════════════════════════════
7
8set -euo pipefail
9
10# ── Configuration ──────────────────────────────────────
11COMPOSE_FILE="docker/docker-compose.yml"
12BASE_URL="http://localhost"
13MAX_WAIT_SECONDS=90
14POLL_INTERVAL=5
15
16# ── Color output ───────────────────────────────────────
17RED='\033[0;31m'
18GREEN='\033[0;32m'
19YELLOW='\033[1;33m'
20NC='\033[0m' # No Color
21
22PASS=0
23FAIL=0
24
25# ── Helper functions ───────────────────────────────────
26
27log_info() { echo -e "${YELLOW}[INFO]${NC} $1"; }
28log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; PASS=$((PASS + 1)); }
29log_fail() { echo -e "${RED}[FAIL]${NC} $1"; FAIL=$((FAIL + 1)); }
30
31check_status() {
32 local description="$1"
33 local url="$2"
34 local expected_status="$3"
35
36 local actual_status
37 actual_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url" 2>/dev/null || echo "000")
38
39 if [ "$actual_status" = "$expected_status" ]; then
40 log_pass "$description → HTTP $actual_status (expected $expected_status)"
41 else
42 log_fail "$description → HTTP $actual_status (expected $expected_status) — URL: $url"
43 fi
44}
45
46check_json_field() {
47 local description="$1"
48 local url="$2"
49 local field="$3"
50
51 local response
52 response=$(curl -s --max-time 10 "$url" 2>/dev/null || echo "{}")
53
54 # Use node to parse JSON — available on any system running this stack.
55 local value
56 value=$(node -e "
57 try {
58 const data = JSON.parse(process.argv[1]);
59 const keys = process.argv[2].split('.');
60 let val = data;
61 for (const key of keys) { val = val[key]; }
62 process.stdout.write(val !== undefined ? 'found' : 'missing');
63 } catch (e) {
64 process.stdout.write('error');
65 }
66 " "$response" "$field" 2>/dev/null || echo "error")
67
68 if [ "$value" = "found" ]; then
69 log_pass "$description → field '$field' present"
70 else
71 log_fail "$description → field '$field' missing or response unparseable"
72 fi
73}
74
75wait_for_healthy() {
76 local service_name="$1"
77 local url="$2"
78 local elapsed=0
79
80 log_info "Waiting for $service_name to become healthy (up to ${MAX_WAIT_SECONDS}s)..."
81
82 while [ $elapsed -lt $MAX_WAIT_SECONDS ]; do
83 local actual_status
84 actual_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url" 2>/dev/null || echo "000")
85
86 # Accept 200 (health ok) or 401 (auth endpoint, service is up) as "alive"
87 if [ "$actual_status" = "200" ] || [ "$actual_status" = "401" ]; then
88 log_info "$service_name is responding (HTTP $actual_status) after ${elapsed}s"
89 return 0
90 fi
91
92 sleep $POLL_INTERVAL
93 elapsed=$((elapsed + POLL_INTERVAL))
94 log_info " ... still waiting (${elapsed}s / ${MAX_WAIT_SECONDS}s, HTTP $actual_status)"
95 done
96
97 echo -e "${RED}[ERROR]${NC} $service_name did not become healthy within ${MAX_WAIT_SECONDS}s"
98 return 1
99}
100
101show_logs_for_service() {
102 local service="$1"
103 echo ""
104 echo -e "${YELLOW}═══ Logs for: $service ═══${NC}"
105 docker compose -f "$COMPOSE_FILE" logs --tail=30 "$service" 2>/dev/null || true
106 echo ""
107}
108
109# ── Main ────────────────────────────────────────────────
110
111echo ""
112echo -e "${YELLOW}╔══════════════════════════════════════════╗${NC}"
113echo -e "${YELLOW}║ ATlast Docker Stack Smoke Test ║${NC}"
114echo -e "${YELLOW}╚══════════════════════════════════════════╝${NC}"
115echo ""
116
117# ── Step 1: Build and start the stack ──────────────────
118log_info "Building and starting the stack..."
119docker compose -f "$COMPOSE_FILE" up --build -d
120
121echo ""
122log_info "Stack started. Waiting for services to initialize..."
123echo ""
124
125# ── Step 2: Wait for API health endpoint ───────────────
126if ! wait_for_healthy "API" "$BASE_URL/api/health"; then
127 echo ""
128 echo -e "${RED}API did not start. Showing logs for debugging:${NC}"
129 show_logs_for_service "api"
130 show_logs_for_service "database"
131 show_logs_for_service "redis"
132 exit 1
133fi
134
135echo ""
136echo -e "${YELLOW}─── Running endpoint checks ───${NC}"
137echo ""
138
139# ── Step 3: API health check ───────────────────────────
140check_status \
141 "GET /api/health → 200 OK" \
142 "$BASE_URL/api/health" \
143 "200"
144
145check_json_field \
146 "GET /api/health → has 'status' field" \
147 "$BASE_URL/api/health" \
148 "status"
149
150# ── Step 4: Auth endpoints (unauthenticated) ───────────
151
152# 401 proves: API is running AND auth middleware is working correctly.
153# A missing session returning 401 is the expected, correct behavior.
154check_status \
155 "GET /api/auth/session → 401 (no session cookie, auth middleware active)" \
156 "$BASE_URL/api/auth/session" \
157 "401"
158
159check_status \
160 "GET /api/auth/client-metadata.json → 200" \
161 "$BASE_URL/api/auth/client-metadata.json" \
162 "200"
163
164check_json_field \
165 "GET /api/auth/client-metadata.json → has 'client_id' field" \
166 "$BASE_URL/api/auth/client-metadata.json" \
167 "client_id"
168
169check_status \
170 "GET /api/auth/jwks → 200" \
171 "$BASE_URL/api/auth/jwks" \
172 "200"
173
174check_json_field \
175 "GET /api/auth/jwks → has 'keys' field" \
176 "$BASE_URL/api/auth/jwks" \
177 "keys"
178
179# ── Step 5: Frontend is served ─────────────────────────
180check_status \
181 "GET / → 200 (frontend HTML served by nginx)" \
182 "$BASE_URL/" \
183 "200"
184
185# ── Step 6: Docker service health summary ──────────────
186echo ""
187echo -e "${YELLOW}─── Docker service status ───${NC}"
188docker compose -f "$COMPOSE_FILE" ps
189echo ""
190
191# ── Step 7: Show results ───────────────────────────────
192echo -e "${YELLOW}─── Results ────────────────────────────────────${NC}"
193echo -e " ${GREEN}Passed: $PASS${NC}"
194echo -e " ${RED}Failed: $FAIL${NC}"
195echo ""
196
197if [ $FAIL -gt 0 ]; then
198 echo -e "${RED}Smoke test FAILED ($FAIL checks failed).${NC}"
199 echo ""
200 echo "Showing logs for potentially failing services:"
201
202 for service in api worker database redis frontend; do
203 STATUS=$(docker compose -f "$COMPOSE_FILE" ps --format "{{.Service}} {{.Status}}" 2>/dev/null \
204 | grep "^$service " | awk '{print $2}' || echo "unknown")
205 if [[ "$STATUS" != *"Up"* ]] || [[ "$STATUS" == *"unhealthy"* ]]; then
206 show_logs_for_service "$service"
207 fi
208 done
209
210 exit 1
211else
212 echo -e "${GREEN}All smoke tests passed!${NC}"
213 echo ""
214 echo "The stack is running. Access the app at: $BASE_URL"
215 exit 0
216fi