Skip to content

Commit eca4020

Browse files
feat(restheart-mongo): keploy compat lane sample (scaffold)
Mirrors the doccano-django sample shape: the sample owns orchestration (compose / bootstrap / traffic / coverage), keploy CI lanes consume it as a thin wrapper. This is a SCAFFOLD — the full traffic loop driven by the existing keploy/enterprise lane (`compat_trigger_record_traffic` in .ci/scripts/restheart-linux.sh, ~600 lines covering CRUD on /<db>/<coll> + GraphQL + files + ACL + users + bulk + aggregations) needs to be ported into flow.sh::restheart_record_traffic in a follow-up. The current loop is deliberately minimal (CRUD on a seed collection) which is enough to prove the sample boots end-to-end without keploy. Layout: Dockerfile — pin to softinstigate/restheart:9.2.1 docker-compose.yml — mongo:7 + restheart:9.2.1, env-driven flow.sh — bootstrap | record-traffic | coverage | list-routes keploy.yml.template — globalNoise for _etag/_oid/lastModified/Date README.md — handoff + status notes Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent f3fc534 commit eca4020

5 files changed

Lines changed: 332 additions & 0 deletions

File tree

restheart-mongo/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Thin wrapper around RESTHeart's official image at the version
2+
# this sample tracks. Pin lives here so a future RESTHeart release
3+
# is a one-line retag, not a hunt across keploy CI lanes.
4+
#
5+
# Upstream: https://github.com/SoftInstigate/restheart
6+
# Image: docker.io/softinstigate/restheart:9.2.1
7+
FROM softinstigate/restheart:9.2.1

restheart-mongo/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# restheart-mongo — keploy compat lane sample (work in progress)
2+
3+
Minimum reproducer scaffold for the RESTHeart / MongoDB compat lane. Mirrors the architectural pattern of the [doccano-django sample in `samples-python`](https://github.com/keploy/samples-python/tree/main/doccano-django): the sample owns orchestration (compose / bootstrap / traffic / noise filter / coverage), keploy CI lanes consume it as a thin wrapper.
4+
5+
## Status
6+
7+
**This is a SCAFFOLD.** The compose, bootstrap, and a minimal record-traffic loop work end-to-end against bare RESTHeart without keploy in the picture. The full traffic loop the existing keploy/enterprise lane drives (`compat_trigger_record_traffic` in `enterprise/.ci/scripts/restheart-linux.sh`, ~600 lines covering CRUD on `/<db>/<coll>` + GraphQL + files + ACL + users + bulk + aggregations) has **not been ported** into `flow.sh::restheart_record_traffic` yet. Lanes consuming this sample today should either:
8+
9+
1. Port the missing curls into `flow.sh::restheart_record_traffic` (preferred — that's the migration this scaffold is designed around).
10+
2. Or call into `enterprise/.ci/scripts/restheart-linux.sh::compat_trigger_record_traffic` between `flow.sh bootstrap` and `flow.sh coverage` until the migration completes.
11+
12+
See the migration plan in this PR's description / linked issue.
13+
14+
## Layout
15+
16+
```
17+
restheart-mongo/
18+
├── Dockerfile # FROM softinstigate/restheart:9.2.1
19+
├── docker-compose.yml # mongo:7 + restheart:9.2.1, fixed subnet, env-driven
20+
├── flow.sh # bootstrap | record-traffic | coverage | list-routes
21+
├── keploy.yml.template # globalNoise for _etag/_oid/lastModified/Date
22+
└── README.md # this file
23+
```
24+
25+
## Contract
26+
27+
The sample is keploy-independent: `docker compose up && bash flow.sh bootstrap && bash flow.sh record-traffic` runs end-to-end against bare RESTHeart. Lane scripts wrap that exact same path inside `keploy record` / `keploy test`.
28+
29+
* `bootstrap` — wait for RESTHeart to start serving, PUT the test database + collection so subsequent reads have something to find.
30+
* `record-traffic` — drive RESTHeart's REST surface. Every call is logged to `${RESTHEART_FIRED_ROUTES_FILE}` (when set) so `coverage` has a numerator without a keploy recording.
31+
* `coverage` — emits `(method, path)` coverage. Denominator is curated from RESTHeart's pattern-based mount table (see `restheart_list_routes` in `flow.sh`); not file-system-derivable like Next.js, so the list lives in source and must be updated alongside `record-traffic`.
32+
* `list-routes` — diagnostic; prints the route table.
33+
34+
## Local run
35+
36+
```sh
37+
docker compose up -d
38+
bash flow.sh bootstrap 240
39+
RESTHEART_FIRED_ROUTES_FILE=/tmp/fired.log bash flow.sh record-traffic
40+
RESTHEART_FIRED_ROUTES_FILE=/tmp/fired.log bash flow.sh coverage
41+
docker compose down -v
42+
```
43+
44+
## Consumers
45+
46+
Lanes pinning to this sample (pinned via `--branch feat/restheart-mongo-sample` until merge):
47+
48+
* `keploy/enterprise` `.woodpecker/restheart-linux.yml` — being slimmed in a follow-up PR.
49+
* No `keploy/integrations` consumer today; could be added if a RESTHeart-flavoured Mongo wire bug surfaces.

restheart-mongo/docker-compose.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# restheart-mongo sample compose. RESTHeart 9.x + MongoDB 7 on a
2+
# fixed subnet, every name env-driven so multiple matrix cells
3+
# can run in parallel on the same docker daemon.
4+
services:
5+
restheart:
6+
build:
7+
context: .
8+
dockerfile: Dockerfile
9+
container_name: ${RESTHEART_APP_CONTAINER:-restheart_app}
10+
init: true
11+
stop_grace_period: 5s
12+
ports:
13+
- "${RESTHEART_APP_PORT:-8080}:8080"
14+
environment:
15+
RHO: >
16+
/mclient/connection-string->"mongodb://${RESTHEART_MONGO_IP:-172.36.0.10}:27017",
17+
/core/log-level->"INFO"
18+
depends_on:
19+
mongo:
20+
condition: service_healthy
21+
networks:
22+
- restheart-net
23+
24+
mongo:
25+
image: mongo:7
26+
container_name: ${RESTHEART_MONGO_CONTAINER:-restheart_mongo}
27+
stop_grace_period: 5s
28+
healthcheck:
29+
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
30+
interval: 5s
31+
timeout: 5s
32+
retries: 20
33+
volumes:
34+
- restheart-mongo-data:/data/db
35+
networks:
36+
restheart-net:
37+
ipv4_address: ${RESTHEART_MONGO_IP:-172.36.0.10}
38+
39+
networks:
40+
restheart-net:
41+
driver: bridge
42+
ipam:
43+
config:
44+
- subnet: ${RESTHEART_NETWORK_SUBNET:-172.36.0.0/24}
45+
46+
volumes:
47+
restheart-mongo-data:

restheart-mongo/flow.sh

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# keploy.yml template for the restheart-mongo sample.
2+
#
3+
# globalNoise covers fields whose value is non-deterministic
4+
# across record/replay:
5+
#
6+
# header.Date runtime-stamped
7+
# body._etag RESTHeart auto-stamped on each
8+
# document; changes per write
9+
# body._oid / body._id server-generated ObjectIds
10+
# (when not set by client)
11+
# body.lastModified auto-now timestamp
12+
#
13+
# Centralised here so a future RESTHeart version that adds another
14+
# auto-stamped field is one edit, not a fan-out across lane scripts.
15+
test:
16+
globalNoise:
17+
global:
18+
header.Date: []
19+
body._etag: []
20+
body._oid: []
21+
body.lastModified: []

0 commit comments

Comments
 (0)