|
| 1 | +#!/bin/bash |
| 2 | +# Post-deployment smoke test for openboot.dev API. |
| 3 | +# Tests that critical endpoints return the expected response shape. |
| 4 | +# |
| 5 | +# Usage: |
| 6 | +# ./scripts/smoke-test-api.sh # test production |
| 7 | +# ./scripts/smoke-test-api.sh http://localhost:5173 # test local |
| 8 | +# |
| 9 | +set -euo pipefail |
| 10 | + |
| 11 | +BASE_URL="${1:-https://openboot.dev}" |
| 12 | +PASS=0 |
| 13 | +FAIL=0 |
| 14 | + |
| 15 | +pass() { PASS=$((PASS + 1)); echo " ✓ $1"; } |
| 16 | +fail() { FAIL=$((FAIL + 1)); echo " ✗ $1: $2"; } |
| 17 | + |
| 18 | +echo "Smoke testing $BASE_URL" |
| 19 | +echo "" |
| 20 | + |
| 21 | +# --- Health --- |
| 22 | +echo "=== /api/health ===" |
| 23 | +HEALTH=$(curl -sf "$BASE_URL/api/health" 2>/dev/null || echo '{}') |
| 24 | +if echo "$HEALTH" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d.get('status') in ('healthy','degraded')" 2>/dev/null; then |
| 25 | + pass "health endpoint responds" |
| 26 | +else |
| 27 | + fail "health endpoint" "unexpected response" |
| 28 | +fi |
| 29 | + |
| 30 | +# --- /api/packages --- |
| 31 | +echo "" |
| 32 | +echo "=== /api/packages ===" |
| 33 | +PKGS=$(curl -sf "$BASE_URL/api/packages" 2>/dev/null || echo '{}') |
| 34 | + |
| 35 | +# Has packages array |
| 36 | +if echo "$PKGS" | python3 -c "import sys,json; d=json.load(sys.stdin); assert len(d['packages']) > 50" 2>/dev/null; then |
| 37 | + pass "returns 50+ packages" |
| 38 | +else |
| 39 | + fail "package count" "expected 50+ packages" |
| 40 | +fi |
| 41 | + |
| 42 | +# Each package has required fields |
| 43 | +if echo "$PKGS" | python3 -c " |
| 44 | +import sys,json |
| 45 | +d=json.load(sys.stdin) |
| 46 | +p=d['packages'][0] |
| 47 | +for f in ('name','desc','category','type','installer'): |
| 48 | + assert f in p, f'missing {f}' |
| 49 | +assert p['installer'] in ('formula','cask','npm'), f'bad installer: {p[\"installer\"]}' |
| 50 | +" 2>/dev/null; then |
| 51 | + pass "packages have name, desc, category, type, installer" |
| 52 | +else |
| 53 | + fail "package shape" "missing required fields" |
| 54 | +fi |
| 55 | + |
| 56 | +# Installer breakdown has all three types |
| 57 | +if echo "$PKGS" | python3 -c " |
| 58 | +import sys,json |
| 59 | +d=json.load(sys.stdin) |
| 60 | +types = set(p['installer'] for p in d['packages']) |
| 61 | +assert 'formula' in types and 'cask' in types and 'npm' in types |
| 62 | +" 2>/dev/null; then |
| 63 | + pass "has formula, cask, and npm installers" |
| 64 | +else |
| 65 | + fail "installer types" "missing formula/cask/npm" |
| 66 | +fi |
| 67 | + |
| 68 | +# --- /api/homebrew/search --- |
| 69 | +echo "" |
| 70 | +echo "=== /api/homebrew/search ===" |
| 71 | +SEARCH=$(curl -sf "$BASE_URL/api/homebrew/search?q=git" 2>/dev/null || echo '{}') |
| 72 | +if echo "$SEARCH" | python3 -c " |
| 73 | +import sys,json |
| 74 | +d=json.load(sys.stdin) |
| 75 | +assert 'formulae' in d or 'results' in d |
| 76 | +" 2>/dev/null; then |
| 77 | + pass "homebrew search responds" |
| 78 | +else |
| 79 | + fail "homebrew search" "unexpected response shape" |
| 80 | +fi |
| 81 | + |
| 82 | +# --- Config endpoint (using a known public config if available) --- |
| 83 | +echo "" |
| 84 | +echo "=== Config endpoint ===" |
| 85 | +# Try the official openboot config first, fall back to any public config. |
| 86 | +CONFIG=$(curl -sf "$BASE_URL/openboot/developer/config" 2>/dev/null || echo '') |
| 87 | +if [ -z "$CONFIG" ]; then |
| 88 | + # Try fetching public configs list. |
| 89 | + CONFIG=$(curl -sf "$BASE_URL/api/configs/public" 2>/dev/null | python3 -c " |
| 90 | +import sys,json |
| 91 | +configs=json.load(sys.stdin) |
| 92 | +if configs and len(configs)>0: |
| 93 | + c=configs[0] |
| 94 | + print(c.get('username',''),c.get('slug','')) |
| 95 | +" 2>/dev/null || echo '') |
| 96 | + if [ -n "$CONFIG" ]; then |
| 97 | + read -r USER SLUG <<< "$CONFIG" |
| 98 | + CONFIG=$(curl -sf "$BASE_URL/$USER/$SLUG/config" 2>/dev/null || echo '') |
| 99 | + fi |
| 100 | +fi |
| 101 | + |
| 102 | +if [ -n "$CONFIG" ] && [ "$CONFIG" != "{}" ]; then |
| 103 | + # packages should be objects with name+desc |
| 104 | + if echo "$CONFIG" | python3 -c " |
| 105 | +import sys,json |
| 106 | +d=json.load(sys.stdin) |
| 107 | +assert 'packages' in d |
| 108 | +assert 'casks' in d |
| 109 | +assert 'taps' in d |
| 110 | +assert 'npm' in d |
| 111 | +if len(d['packages']) > 0: |
| 112 | + p = d['packages'][0] |
| 113 | + assert isinstance(p, dict), f'expected object, got {type(p).__name__}' |
| 114 | + assert 'name' in p, 'missing name' |
| 115 | + assert 'desc' in p, 'missing desc' |
| 116 | +" 2>/dev/null; then |
| 117 | + pass "config returns {name, desc} objects for packages" |
| 118 | + else |
| 119 | + fail "config format" "packages not in expected {name, desc} format" |
| 120 | + fi |
| 121 | + |
| 122 | + # taps should be plain strings |
| 123 | + if echo "$CONFIG" | python3 -c " |
| 124 | +import sys,json |
| 125 | +d=json.load(sys.stdin) |
| 126 | +if len(d.get('taps',[])) > 0: |
| 127 | + assert isinstance(d['taps'][0], str), 'taps should be strings' |
| 128 | +" 2>/dev/null; then |
| 129 | + pass "taps are plain strings" |
| 130 | + else |
| 131 | + fail "taps format" "expected string array" |
| 132 | + fi |
| 133 | +else |
| 134 | + echo " - skipped (no public config found)" |
| 135 | +fi |
| 136 | + |
| 137 | +# --- Summary --- |
| 138 | +echo "" |
| 139 | +TOTAL=$((PASS + FAIL)) |
| 140 | +echo "Results: $PASS/$TOTAL passed" |
| 141 | +if [ "$FAIL" -gt 0 ]; then |
| 142 | + echo "FAILED" |
| 143 | + exit 1 |
| 144 | +else |
| 145 | + echo "ALL PASSED" |
| 146 | +fi |
0 commit comments