Skip to content

Commit 9b22d52

Browse files
committed
feat(dev): add local api test runner
1 parent 6a4ae93 commit 9b22d52

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ api/venv
77

88
# development helper scripts
99
/*.sh
10+
/.cache/
1011

1112
# local certificates
1213
/certs/

api/api/settings_quick_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
},
1818
}
1919

20+
test_db_name = os.environ.get("DESECSTACK_DJANGO_TEST_DB_NAME")
21+
if test_db_name:
22+
DATABASES["default"]["TEST"] = {"NAME": test_db_name}
23+
2024
# avoid computationally expensive password hashing for tests
2125
PASSWORD_HASHERS = [
2226
"django.contrib.auth.hashers.MD5PasswordHasher",

run-api-tests-stack.sh

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
COMPOSE="docker compose -f ${ROOT_DIR}/docker-compose.yml -f ${ROOT_DIR}/docker-compose.test-api.yml"
6+
BUILD_IMAGES_STACK=(api dbapi nslord nsmaster dblord dbmaster)
7+
BUILD_IMAGES_DB=(dbapi)
8+
KEEP=0
9+
BUILD=1
10+
MODE="host"
11+
PROD_DB=0
12+
PROD_USER="root"
13+
PROD_HOST="digga.desec.io"
14+
PROD_REFRESH=0
15+
CACHE_DIR="${ROOT_DIR}/.cache/prod-db"
16+
CACHE_FILE="${CACHE_DIR}/dbapi.sql.gz"
17+
18+
usage() {
19+
cat <<'EOF'
20+
Usage: ./run-api-tests-stack.sh [--no-build] [--keep] [--docker] [--prod-db] [--prod-user USER] [--prod-host HOST] [--refresh-prod-db]
21+
--no-build Skip docker image build step
22+
--keep Do not tear down containers/volumes after tests
23+
--docker Run API tests inside the api container (CI-style)
24+
--prod-db Download a logical dbapi dump from production and load it locally
25+
--prod-user SSH username for prod (default: root)
26+
--prod-host SSH hostname for prod (default: desec.io)
27+
--refresh-prod-db Re-download prod db dump even if cache exists
28+
EOF
29+
}
30+
31+
while [[ $# -gt 0 ]]; do
32+
case "$1" in
33+
--no-build) BUILD=0 ;;
34+
--keep) KEEP=1 ;;
35+
--docker) MODE="docker" ;;
36+
--prod-db) PROD_DB=1 ;;
37+
--prod-user)
38+
shift
39+
PROD_USER="${1:-}"
40+
[[ -n "$PROD_USER" ]] || { echo "Missing value for --prod-user" >&2; exit 1; }
41+
;;
42+
--prod-host)
43+
shift
44+
PROD_HOST="${1:-}"
45+
[[ -n "$PROD_HOST" ]] || { echo "Missing value for --prod-host" >&2; exit 1; }
46+
;;
47+
--refresh-prod-db) PROD_REFRESH=1 ;;
48+
-h|--help) usage; exit 0 ;;
49+
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
50+
esac
51+
shift
52+
done
53+
54+
prod_remote_script() {
55+
cat <<'EOF'
56+
set -euo pipefail
57+
cd /root/desec-stack
58+
docker compose -f docker-compose.yml exec -T dbapi pg_dump -Fc -U desec desec | gzip -c
59+
EOF
60+
}
61+
62+
download_prod_dbapi() {
63+
mkdir -p "$CACHE_DIR"
64+
if [[ "$PROD_REFRESH" -eq 0 && -f "$CACHE_FILE" ]]; then
65+
echo "Using cached prod db dump at ${CACHE_FILE}"
66+
return
67+
fi
68+
69+
local prod_ssh="${PROD_USER}@${PROD_HOST}"
70+
echo "About to run the following read-only commands on ${prod_ssh}:"
71+
prod_remote_script
72+
read -r -p "Continue? [y/N] " reply
73+
case "$reply" in
74+
y|Y|yes|YES) ;;
75+
*) echo "Aborted." >&2; exit 1 ;;
76+
esac
77+
78+
local tmp_file
79+
local old_umask
80+
old_umask="$(umask)"
81+
umask 077
82+
tmp_file="$(mktemp "${CACHE_FILE}.tmp.XXXXXX")"
83+
umask "$old_umask"
84+
if prod_remote_script | ssh -4 "$prod_ssh" "bash -s" > "$tmp_file"; then
85+
mv "$tmp_file" "$CACHE_FILE"
86+
echo "Saved prod db dump to ${CACHE_FILE}"
87+
else
88+
rm -f "$tmp_file"
89+
echo "Failed to download prod db dump; cache not updated." >&2
90+
exit 1
91+
fi
92+
}
93+
94+
restore_dbapi_from_cache() {
95+
if [[ ! -f "$CACHE_FILE" ]]; then
96+
echo "Missing cache file: ${CACHE_FILE}" >&2
97+
exit 1
98+
fi
99+
$COMPOSE exec -T dbapi sh -c "psql -U postgres -d postgres -c \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='desec' AND pid <> pg_backend_pid();\""
100+
$COMPOSE exec -T dbapi sh -c "psql -U postgres -d postgres -c \"DROP DATABASE IF EXISTS desec;\""
101+
$COMPOSE exec -T dbapi sh -c "psql -U postgres -d postgres -c \"CREATE DATABASE desec OWNER desec;\""
102+
gunzip -c "$CACHE_FILE" | $COMPOSE exec -T dbapi sh -c "pg_restore -U desec -d desec --no-owner --no-acl --role=desec"
103+
}
104+
105+
cleanup() {
106+
$COMPOSE ps || true
107+
$COMPOSE down -v || true
108+
}
109+
110+
if [[ "$KEEP" -eq 0 ]]; then
111+
trap cleanup EXIT
112+
fi
113+
114+
if [[ "$MODE" == "docker" ]]; then
115+
if [[ "$PROD_DB" -eq 1 ]]; then
116+
echo "--prod-db is only supported for host-mode tests." >&2
117+
exit 1
118+
fi
119+
if [[ "$BUILD" -eq 1 ]]; then
120+
$COMPOSE build "${BUILD_IMAGES_STACK[@]}"
121+
fi
122+
$COMPOSE run --rm api bash -c "./entrypoint-tests.sh"
123+
else
124+
if [[ "$BUILD" -eq 1 ]]; then
125+
$COMPOSE build "${BUILD_IMAGES_DB[@]}"
126+
fi
127+
$COMPOSE up -d dbapi
128+
(
129+
set -a
130+
source "${ROOT_DIR}/.env"
131+
set +a
132+
export DJANGO_SETTINGS_MODULE=api.settings_quick_test
133+
export DESECSTACK_DJANGO_TEST=1
134+
cd "${ROOT_DIR}/api"
135+
if [[ -x "./venv/bin/python" ]]; then
136+
# Use project venv when present.
137+
source "./venv/bin/activate"
138+
else
139+
echo "Missing venv at ${ROOT_DIR}/api/venv. Create it with: cd api && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt" >&2
140+
exit 1
141+
fi
142+
# Avoid local psql dependency by checking readiness inside the DB container.
143+
wait_seconds=120
144+
start_ts=$(date +%s)
145+
while true; do
146+
if $COMPOSE exec -T dbapi sh -c "command -v pg_isready >/dev/null 2>&1" >/dev/null 2>&1; then
147+
if $COMPOSE exec -T dbapi pg_isready -U "${DESECSTACK_DBAPI_USER:-desec}" -h 127.0.0.1 -p 5432 >/dev/null 2>&1; then
148+
break
149+
fi
150+
else
151+
if $COMPOSE exec -T dbapi sh -c "PGPASSWORD='${DESECSTACK_DBAPI_PASSWORD_desec:-}' psql -U '${DESECSTACK_DBAPI_USER:-desec}' -h 127.0.0.1 -p 5432 -d postgres -c 'select 1' >/dev/null 2>&1"; then
152+
break
153+
fi
154+
fi
155+
156+
now_ts=$(date +%s)
157+
if (( now_ts - start_ts > wait_seconds )); then
158+
echo "Timed out waiting for Postgres to become ready."
159+
$COMPOSE ps
160+
$COMPOSE logs --tail 80 dbapi || true
161+
exit 1
162+
fi
163+
echo "Postgres is unavailable - sleeping"
164+
sleep 2
165+
done
166+
test_args=()
167+
if [[ "$PROD_DB" -eq 1 ]]; then
168+
download_prod_dbapi
169+
restore_dbapi_from_cache
170+
export DESECSTACK_DJANGO_TEST_DB_NAME=desec
171+
test_args+=(--keepdb)
172+
fi
173+
python3 manage.py test "${test_args[@]}"
174+
)
175+
fi

0 commit comments

Comments
 (0)