|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# flow.sh — keploy-independent orchestration for the |
| 4 | +# restheart-mongo sample. Modeled on |
| 5 | +# samples-python/doccano-django/flow.sh. |
| 6 | +# |
| 7 | +# Subcommands: |
| 8 | +# bootstrap — RESTHeart's default config has no admin auth |
| 9 | +# setup needed; the bootstrap step here just |
| 10 | +# creates the test database and seed |
| 11 | +# collections so subsequent reads have |
| 12 | +# something to find. |
| 13 | +# record-traffic — drive RESTHeart's REST surface (Mongo / GraphQL |
| 14 | +# / files / users / acl). Fire-and-forget; |
| 15 | +# keploy is the assertion layer at replay. |
| 16 | +# coverage — report (method, path) coverage. Denominator is |
| 17 | +# derived from RESTHeart's known route-mounts |
| 18 | +# (see SCOPE_PATHS in restheart_list_routes). |
| 19 | +# list-routes — print the route table the coverage report |
| 20 | +# uses as its denominator. |
| 21 | +# |
| 22 | +# HANDOFF NOTE: SCAFFOLD. The full traffic loop the existing keploy |
| 23 | +# lane drives (`compat_trigger_record_traffic` in |
| 24 | +# enterprise/.ci/scripts/restheart-linux.sh, ~600 lines covering |
| 25 | +# CRUD on /<db>/<coll> + GraphQL + files + ACL + users + bulk + |
| 26 | +# aggregations) needs to be ported into |
| 27 | +# `restheart_record_traffic` here. The stub below covers enough |
| 28 | +# to prove the sample boots end-to-end without keploy. See the |
| 29 | +# migration plan in the PR description / linked issue. |
| 30 | +set -Eeuo pipefail |
| 31 | + |
| 32 | +RESTHEART_APP_PORT="${RESTHEART_APP_PORT:-8080}" |
| 33 | +RESTHEART_APP_CONTAINER="${RESTHEART_APP_CONTAINER:-restheart_app}" |
| 34 | +RESTHEART_MONGO_CONTAINER="${RESTHEART_MONGO_CONTAINER:-restheart_mongo}" |
| 35 | +RESTHEART_DB="${RESTHEART_DB:-keploy}" |
| 36 | +RESTHEART_PHASE="${RESTHEART_PHASE:-local}" |
| 37 | +RESTHEART_FIRED_ROUTES_FILE="${RESTHEART_FIRED_ROUTES_FILE:-}" |
| 38 | + |
| 39 | +# RESTHeart 9.x ships with an admin user (admin/secret) for protected |
| 40 | +# endpoints; the unauthenticated paths are fine for the smoke set we |
| 41 | +# drive in record-traffic. Override RESTHEART_ADMIN_AUTH to add |
| 42 | +# `Authorization: Basic <b64>` to authenticated calls when porting |
| 43 | +# the full lane traffic. |
| 44 | +RESTHEART_ADMIN_AUTH="${RESTHEART_ADMIN_AUTH:-Basic YWRtaW46c2VjcmV0}" |
| 45 | + |
| 46 | +base="http://127.0.0.1:${RESTHEART_APP_PORT}" |
| 47 | +h_json='Content-Type: application/json' |
| 48 | + |
| 49 | +log_fired() { |
| 50 | + [ -z "$RESTHEART_FIRED_ROUTES_FILE" ] && return 0 |
| 51 | + printf '%s %s\n' "$1" "$2" >>"$RESTHEART_FIRED_ROUTES_FILE" |
| 52 | +} |
| 53 | + |
| 54 | +restheart_wait_for_app() { |
| 55 | + local timeout=${1:-180} |
| 56 | + local start_ts code |
| 57 | + start_ts=$(date +%s) |
| 58 | + while true; do |
| 59 | + code=$(curl -sS -o /dev/null -w '%{http_code}' "${base}/" 2>/dev/null || echo "") |
| 60 | + # 401 (auth required on root) is a SUCCESS signal — it |
| 61 | + # means RESTHeart is up and responding to HTTP. |
| 62 | + if [ "$code" = "200" ] || [ "$code" = "401" ]; then return 0; fi |
| 63 | + if [ $(( $(date +%s) - start_ts )) -ge "$timeout" ]; then |
| 64 | + echo "restheart_wait_for_app: timed out (last code: ${code:-<empty>})" >&2 |
| 65 | + return 1 |
| 66 | + fi |
| 67 | + sleep 2 |
| 68 | + done |
| 69 | +} |
| 70 | + |
| 71 | +restheart_bootstrap() { |
| 72 | + local timeout=${1:-180} |
| 73 | + restheart_wait_for_app "$timeout" |
| 74 | + |
| 75 | + # Create the test database. PUT on /<db> is idempotent — |
| 76 | + # 201 first time, 200 on subsequent runs. |
| 77 | + curl -sS -o /dev/null -H "$RESTHEART_ADMIN_AUTH" -X PUT "${base}/${RESTHEART_DB}" || true |
| 78 | + # Seed a collection so reads have something to find. |
| 79 | + curl -sS -o /dev/null -H "$RESTHEART_ADMIN_AUTH" -X PUT "${base}/${RESTHEART_DB}/items" || true |
| 80 | + echo "restheart_bootstrap: db=${RESTHEART_DB} ready" |
| 81 | +} |
| 82 | + |
| 83 | +restheart_record_traffic() { |
| 84 | + restheart_wait_for_app 60 |
| 85 | + |
| 86 | + log_fired GET "$base/" |
| 87 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" "$base/" >/dev/null || true |
| 88 | + |
| 89 | + log_fired GET "$base/${RESTHEART_DB}" |
| 90 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" "$base/${RESTHEART_DB}" >/dev/null || true |
| 91 | + |
| 92 | + log_fired GET "$base/${RESTHEART_DB}/items" |
| 93 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" "$base/${RESTHEART_DB}/items" >/dev/null || true |
| 94 | + |
| 95 | + # Insert a document. |
| 96 | + log_fired POST "$base/${RESTHEART_DB}/items" |
| 97 | + curl -fsS -H "$RESTHEART_ADMIN_AUTH" -H "$h_json" -X POST \ |
| 98 | + "$base/${RESTHEART_DB}/items" \ |
| 99 | + -d "{\"_id\":\"keploy-${RESTHEART_PHASE}\",\"name\":\"sample item\",\"score\":42}" >/dev/null || true |
| 100 | + |
| 101 | + # Read it back. |
| 102 | + log_fired GET "$base/${RESTHEART_DB}/items/keploy-${RESTHEART_PHASE}" |
| 103 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" \ |
| 104 | + "$base/${RESTHEART_DB}/items/keploy-${RESTHEART_PHASE}" >/dev/null || true |
| 105 | + |
| 106 | + # Update it. |
| 107 | + log_fired PATCH "$base/${RESTHEART_DB}/items/keploy-${RESTHEART_PHASE}" |
| 108 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" -H "$h_json" -X PATCH \ |
| 109 | + "$base/${RESTHEART_DB}/items/keploy-${RESTHEART_PHASE}" \ |
| 110 | + -d '{"$set":{"score":100}}' >/dev/null || true |
| 111 | + |
| 112 | + # Aggregation surface. |
| 113 | + log_fired GET "$base/${RESTHEART_DB}/items/_size" |
| 114 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" "$base/${RESTHEART_DB}/items/_size" >/dev/null || true |
| 115 | + log_fired GET "$base/${RESTHEART_DB}/_meta" |
| 116 | + curl -sS -H "$RESTHEART_ADMIN_AUTH" "$base/${RESTHEART_DB}/_meta" >/dev/null || true |
| 117 | +} |
| 118 | + |
| 119 | +# RESTHeart's routes are pattern-mount based, not file-system |
| 120 | +# based. The denominator is curated here from the upstream docs + |
| 121 | +# the routes the lane intends to exercise. Update this list when |
| 122 | +# adding new traffic to record-traffic so the coverage stays in |
| 123 | +# lockstep. |
| 124 | +restheart_list_routes() { |
| 125 | + cat <<'ROUTES' |
| 126 | +GET / |
| 127 | +GET /{db} |
| 128 | +PUT /{db} |
| 129 | +DELETE /{db} |
| 130 | +GET /{db}/_meta |
| 131 | +GET /{db}/{coll} |
| 132 | +PUT /{db}/{coll} |
| 133 | +DELETE /{db}/{coll} |
| 134 | +POST /{db}/{coll} |
| 135 | +GET /{db}/{coll}/{docid} |
| 136 | +PUT /{db}/{coll}/{docid} |
| 137 | +PATCH /{db}/{coll}/{docid} |
| 138 | +DELETE /{db}/{coll}/{docid} |
| 139 | +GET /{db}/{coll}/_size |
| 140 | +GET /{db}/{coll}/_aggrs/{name} |
| 141 | +GET /{db}/{coll}/_indexes |
| 142 | +ROUTES |
| 143 | +} |
| 144 | + |
| 145 | +restheart_list_recorded_routes() { |
| 146 | + local f method route |
| 147 | + local found_keploy=0 |
| 148 | + while IFS= read -r f; do |
| 149 | + found_keploy=1 |
| 150 | + method=$(awk '/^ method:/{print $2; exit}' "$f") |
| 151 | + route=$(awk '/^ url:/{print $2; exit}' "$f") |
| 152 | + route="${route%%\?*}" |
| 153 | + case "$route" in http://*|https://*) route="/${route#*://*/}" ;; esac |
| 154 | + if [ -n "$method" ] && [ -n "$route" ]; then echo "$method $route"; fi |
| 155 | + done < <(find keploy -type f -path '*/tests/*.yaml' 2>/dev/null) | sort -u |
| 156 | + if [ "$found_keploy" = "1" ]; then return 0; fi |
| 157 | + |
| 158 | + if [ -n "$RESTHEART_FIRED_ROUTES_FILE" ] && [ -f "$RESTHEART_FIRED_ROUTES_FILE" ]; then |
| 159 | + while IFS= read -r line; do |
| 160 | + method="${line%% *}"; route="${line#* }" |
| 161 | + route="${route%%\?*}" |
| 162 | + case "$route" in http://*|https://*) route="/${route#*://*/}" ;; esac |
| 163 | + [ -n "$method" ] && [ -n "$route" ] && echo "$method $route" |
| 164 | + done <"$RESTHEART_FIRED_ROUTES_FILE" | sort -u |
| 165 | + fi |
| 166 | +} |
| 167 | + |
| 168 | +restheart_report_coverage() { |
| 169 | + local routes_file recorded_file |
| 170 | + routes_file="$(mktemp)"; recorded_file="$(mktemp)" |
| 171 | + restheart_list_routes >"$routes_file" |
| 172 | + restheart_list_recorded_routes >"$recorded_file" |
| 173 | + |
| 174 | + local total covered missing pct |
| 175 | + total=$(wc -l <"$routes_file" | tr -d ' '); covered=0; missing="" |
| 176 | + while IFS= read -r line; do |
| 177 | + local method="${line%% *}" |
| 178 | + local route="${line#* }" |
| 179 | + # Replace {param} placeholders with [^/]+ for matching. |
| 180 | + local pattern |
| 181 | + pattern="^${method} $(printf '%s' "$route" | sed -E 's/\{[^}]+\}/[^\/]+/g')$" |
| 182 | + if grep -qE "$pattern" "$recorded_file"; then |
| 183 | + covered=$((covered + 1)) |
| 184 | + else |
| 185 | + missing+=" ${method} ${route}"$'\n' |
| 186 | + fi |
| 187 | + done <"$routes_file" |
| 188 | + if [ "$total" -gt 0 ]; then |
| 189 | + pct=$(awk -v c="$covered" -v t="$total" 'BEGIN{printf "%.1f", c*100/t}') |
| 190 | + else pct="0.0"; fi |
| 191 | + { |
| 192 | + echo "================ RESTHeart API coverage ================" |
| 193 | + echo "Covered ${covered}/${total} (${pct}%)" |
| 194 | + if [ -n "$missing" ]; then echo "Uncovered:"; printf '%s' "$missing"; fi |
| 195 | + echo "========================================================" |
| 196 | + } | tee "${COVERAGE_REPORT_FILE:-coverage_report.txt}" |
| 197 | + rm -f "$routes_file" "$recorded_file" |
| 198 | +} |
| 199 | + |
| 200 | +case "${1:-}" in |
| 201 | + bootstrap) restheart_bootstrap "${2:-180}" ;; |
| 202 | + record-traffic) restheart_record_traffic ;; |
| 203 | + coverage) restheart_report_coverage ;; |
| 204 | + list-routes) restheart_list_routes ;; |
| 205 | + *) |
| 206 | + echo "usage: $0 {bootstrap|record-traffic|coverage|list-routes}" >&2 |
| 207 | + exit 2 ;; |
| 208 | +esac |
0 commit comments