Skip to content

Commit 1035cf5

Browse files
feat(parse-server-mongo): subcommand flow + env-driven compose + noise template
Make the parse-server-mongo sample directly consumable as a keploy compat-lane sample, on par with samples-typescript/umami-postgres and samples-python/ doccano-django. - flow.sh refactored to subcommand form: bootstrap | record-traffic | coverage | list-routes. bootstrap is idempotent (4xx during signup is treated as already-exists), persists the session token to /tmp/parse-token-${PARSE_PHASE}, and preserves the 3-second post-/health pause that the boot-phase _SCHEMA divergence reproducer needs. - record-traffic ports the broad parse-server REST + GraphQL surface (51 fire calls across users/sessions/classes/roles/files/cloud-functions/ schemas/hooks/graphql/aggregate). Every curl is preceded by log_fired so PARSE_FIRED_ROUTES_FILE captures (method,route) for coverage. The multi-class _SCHEMA mutation (GameScore, PlayerStats, Achievement) is retained so the boot-phase tiebreaker reproducer still triggers. - coverage subcommand reports (method,route) coverage with numerator from keploy/test-set-*/tests/*.yaml when present, else from the fired-routes log. Denominator from a curated 35-route table in parse_list_routes. - docker-compose.yml takes ${VAR:-default} for project name, network name, network subnet, IPs, container names, host:container port, and the internal PORT env. Defaults preserve standalone `docker compose up`; overrides let concurrent matrix cells share a single docker daemon. - keploy.yml.template carries the parse-server noise filter (header.Date, body.objectId, body.sessionToken, body.createdAt, body.updatedAt) so a lane consumer can apply it to keploy.yml after `keploy config --generate`. - README rewritten to document the four subcommands, all env vars, the route surface covered, the boot-phase divergence rationale, and a concurrent-cell run recipe. Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent ac33825 commit 1035cf5

4 files changed

Lines changed: 616 additions & 135 deletions

File tree

parse-server-mongo/README.md

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,151 @@
11
# parse-server-mongo
22

3-
Minimal Parse Server + MongoDB sample. Exists primarily as a falsifying e2e reproducer for the mongo/v2 boot-phase candidate-selection bug — a class of bug where an application issues the same query repeatedly during recording while DB state mutates, and the matcher's score-tied tiebreaker at replay picks the wrong same-shape mock.
3+
Parse Server (`parseplatform/parse-server` 9.x via `parse-server` npm 8.2.x) backed by MongoDB 7. Packaged as a complete keploy compat-lane sample: subcommand-driven traffic, env-driven docker-compose so concurrent matrix cells share a daemon, a noise-filter template that masks the parse-server identifiers that change every run, and a curated route list for coverage reporting.
4+
5+
The lane consumer is **keploy/enterprise** — its `.ci/scripts/parse-server-linux.sh` clones this repo and orchestrates record / replay against `flow.sh`.
46

57
## Layout
68

79
| File | Role |
810
|---|---|
9-
| `index.js` | 20-line Parse Server bootstrap; reads config from env |
11+
| `index.js` | 25-line Parse Server bootstrap; reads config from env |
1012
| `package.json` | Pins `parse-server@8.2.3` and `express@4.21.2` |
1113
| `Dockerfile` | `node:20-bookworm-slim` + `npm install --omit=dev` |
12-
| `docker-compose.yml` | mongo:7 + this sample, on a fixed-IP bridge network |
13-
| `flow.sh` | Minimum reproducer traffic — three HTTP calls |
14+
| `docker-compose.yml` | mongo:7 + this sample, env-driven (defaults preserve standalone `docker compose up`) |
15+
| `flow.sh` | Subcommand traffic driver: `bootstrap | record-traffic | coverage | list-routes` |
16+
| `keploy.yml.template` | Noise filter for parse-server identifiers (`objectId`, `sessionToken`, `createdAt`, `updatedAt`, `Date` header) |
17+
18+
## flow.sh subcommands
19+
20+
```
21+
flow.sh bootstrap [timeout] wait for /parse/health, sign up the fixed user,
22+
capture session token to /tmp/parse-token-${PARSE_PHASE}.
23+
Idempotent: 4xx on already-exists is treated as success.
24+
25+
flow.sh record-traffic drive the broad parse-server REST + GraphQL surface
26+
the recording should capture. Reads the persisted
27+
session token. Honours PARSE_FIRED_ROUTES_FILE.
28+
29+
flow.sh coverage print (method,route) coverage. Numerator from
30+
keploy/test-set-*/tests/*.yaml when present, else
31+
falls back to PARSE_FIRED_ROUTES_FILE.
32+
33+
flow.sh list-routes print the curated route table.
34+
```
35+
36+
### Boot-phase divergence preserved
37+
38+
`bootstrap` + `record-traffic` together still drive the multi-class `_SCHEMA` mutation pattern (GameScore, PlayerStats, Achievement) the focused boot-phase reproducer needs:
39+
- Parse Server runs `find _SCHEMA filter:{}` repeatedly during its boot eager-index sweep.
40+
- `bootstrap` sleeps 3 seconds after `/health` becomes reachable so pre-mutation `find _SCHEMA` snapshots land first.
41+
- `record-traffic` then issues `POST /classes/<NewClass>` for three distinct user-defined classes, each of which lazily inserts the class into `_SCHEMA`, refreshes parse-server's schema cache, and runs `listIndexes` on the new collection. The recording's `find _SCHEMA` mocks span four shapes (system-only + each post-insert state).
42+
43+
At replay, the matcher sees multiple same-shape `find _SCHEMA` candidates with diverging responses. The boot-phase tiebreaker fix in `keploy/integrations` mongo/v2 + `keploy/keploy` mockmanager prefers the earliest candidate and consumes startup-tier mocks on match so the next identical query advances to the next-earliest in chronological order.
44+
45+
## Route surface covered by `record-traffic`
46+
47+
Curated in `parse_list_routes` (`flow.sh list-routes` to print). Covers:
1448

15-
## What the bug is
49+
- **Health / config**: `/health`, `/serverInfo`, `/config`
50+
- **Users**: `POST /users` (signup), `GET /users`, `GET /users/me`, `GET/PUT /users/{id}`, `GET /users?where=...`
51+
- **Login / logout**: `GET /login`, `POST /logout`
52+
- **Sessions**: `GET /sessions`, `GET /sessions/me`, `DELETE /sessions/{id}`
53+
- **Classes / objects**: `POST/GET/PUT/DELETE /classes/{class}` and `/classes/{class}/{id}`, query (`where`), count, keys/order/limit
54+
- **Roles**: `GET/POST /roles`, `GET /roles?where=...`
55+
- **Files**: `POST /files/{name}` for text, JSON, and binary content types
56+
- **Cloud functions / jobs**: `POST /functions/{name}`, `POST /jobs/{name}`
57+
- **Schemas**: `GET /schemas`, `GET /schemas/{class}`, `POST/PUT/DELETE /schemas/{class}`
58+
- **Hooks**: `GET/POST /hooks/functions`, `PUT/DELETE /hooks/functions/{name}`, `GET /hooks/triggers`
59+
- **GraphQL**: `POST /graphql` for query, mutation, and introspection
60+
- **Aggregate**: `GET /aggregate/{class}` (skipped silently if upstream doesn't ship it)
1661

17-
At boot, Parse Server runs `find _SCHEMA filter:{}` repeatedly during its eager-index sweep. While the recording captures these calls, the recording also drives Parse Server through a `POST /parse/classes/GameScore`, which lazily inserts the `GameScore` user-defined class into `_SCHEMA`. From that point onward, `find _SCHEMA` responses diverge — the early calls captured an empty / system-only `_SCHEMA`, the late ones captured `_SCHEMA` with `GameScore` present.
62+
## docker-compose env vars
1863

19-
At replay, the matcher has multiple same-shape candidates with diverging responses. Score-tied tiebreaker decides which one wins. The default tiebreaker picks the candidate closest to `mockWindowMaxReqTimestamp` (the latest recording timestamp), which biases toward late-recording mocks. The booting app then sees post-mutation `_SCHEMA`, takes the steady-state code path, and runs `listIndexes <user-class>` — a query the recording never witnessed, because at record time the user class wasn't in `_SCHEMA` at boot.
64+
All container names, network name, network subnet, IPs, host:container port and internal `PORT` accept `${VAR:-default}` overrides so concurrent matrix cells can share a docker daemon without colliding:
2065

21-
The fix (in `keploy/integrations` mongo/v2 + a companion in `keploy/keploy` mockmanager) inverts the tiebreaker at boot phase to prefer the earliest same-shape candidate, and consumes startup-tier mocks on match so the next identical query advances to the next-earliest in chronological order.
66+
| Var | Default | Purpose |
67+
|---|---|---|
68+
| `PARSE_PROJECT` | `parse-server-mongo` | top-level compose project name |
69+
| `PARSE_NETWORK_NAME` | `parse-server-mongo-net` | docker network name |
70+
| `PARSE_NETWORK_SUBNET` | `172.30.0.0/24` | network subnet |
71+
| `PARSE_MONGO_IP` | `172.30.0.10` | mongo's static IP |
72+
| `PARSE_APP_IP` | `172.30.0.11` | parse-server's static IP |
73+
| `PARSE_MONGO_CONTAINER` | `parse-server-mongo-mongo` | mongo container name |
74+
| `PARSE_APP_CONTAINER` | `parse-server-mongo-app` | parse-server container name |
75+
| `PARSE_IMAGE` | `parse-server-mongo:local` | built image tag |
76+
| `PARSE_HOST_PORT` | `6100` | host port published to localhost |
77+
| `PARSE_CONTAINER_PORT` | `6100` | port parse-server listens on inside the container |
78+
| `PARSE_MOUNT_PATH` | `/parse` | parse-server mount path |
79+
| `PARSE_APP_ID` | `keploy-parse-app` | `X-Parse-Application-Id` |
80+
| `PARSE_MASTER_KEY` | `keploy-parse-master` | `X-Parse-Master-Key` |
81+
| `PARSE_MASTER_KEY_IPS` | `0.0.0.0/0,::0` | master-key IP allowlist |
82+
| `PARSE_SERVER_URL` | `http://localhost:6100/parse` | public server URL |
83+
| `PARSE_DATABASE_URI` | `mongodb://172.30.0.10:27017/parse` | mongo URI |
84+
| `PARSE_ALLOW_CUSTOM_OBJECT_ID` | `1` | accept caller-supplied `objectId` |
85+
86+
## flow.sh env vars
87+
88+
| Var | Default | Purpose |
89+
|---|---|---|
90+
| `APP_PORT` | `6100` | host port to drive traffic against |
91+
| `PARSE_APP_ID` | `keploy-parse-app` | `X-Parse-Application-Id` |
92+
| `PARSE_MASTER_KEY` | `keploy-parse-master` | `X-Parse-Master-Key` |
93+
| `PARSE_MOUNT_PATH` | `/parse` | mount path on the server |
94+
| `PARSE_PHASE` | `record` | tag — names the persisted token slot `/tmp/parse-token-${PARSE_PHASE}` |
95+
| `PARSE_FIXED_USERNAME` | `keploy-user` | pinned signup username |
96+
| `PARSE_FIXED_PASSWORD` | `KeployPass123!` | pinned signup password |
97+
| `PARSE_FIXED_USER_ID` | `keploy-user-id` | pinned `_User` `objectId` |
98+
| `PARSE_FIXED_SCORE_ID` | `keploy-score-id` | pinned `GameScore` `objectId` |
99+
| `PARSE_FIXED_PLAYER_ID` | `keploy-player-id` | pinned `PlayerStats` `objectId` |
100+
| `PARSE_FIXED_ACHIEVEMENT_ID` | `keploy-achievement-id` | pinned `Achievement` `objectId` |
101+
| `PARSE_FIRED_ROUTES_FILE` | _unset_ | if set, every fired curl appends `METHOD /path` |
102+
| `PARSE_TOKEN_FILE` | `/tmp/parse-token-${PARSE_PHASE}` | persisted session token slot |
103+
104+
## keploy.yml.template
105+
106+
`keploy.yml.template` carries the noise filter for parse-server's identifiers:
107+
108+
```yaml
109+
test:
110+
globalNoise:
111+
global:
112+
header.Date: []
113+
body.objectId: []
114+
body.sessionToken: []
115+
body.createdAt: []
116+
body.updatedAt: []
117+
```
118+
119+
A lane consumer copies this onto the generated `keploy.yml` after `keploy config --generate` so replay assertions ignore the request-scoped fields parse-server mints fresh per call.
22120

23121
## Running locally
24122

123+
Standalone (no env vars):
124+
25125
```bash
26126
docker compose up -d
27-
bash flow.sh
127+
bash flow.sh bootstrap 240
128+
PARSE_FIRED_ROUTES_FILE=/tmp/p.log bash flow.sh record-traffic
129+
PARSE_FIRED_ROUTES_FILE=/tmp/p.log bash flow.sh coverage
28130
docker compose down -v
29131
```
30132

31-
## How the e2e lane uses this sample
133+
Concurrent matrix cell:
134+
135+
```bash
136+
PARSE_PROJECT=cell-A \
137+
PARSE_HOST_PORT=7100 PARSE_CONTAINER_PORT=7100 \
138+
PARSE_NETWORK_NAME=parse-cell-A-net \
139+
PARSE_NETWORK_SUBNET=172.31.0.0/24 \
140+
PARSE_MONGO_IP=172.31.0.10 PARSE_APP_IP=172.31.0.11 \
141+
PARSE_MONGO_CONTAINER=parse-cell-A-mongo \
142+
PARSE_APP_CONTAINER=parse-cell-A-app \
143+
PARSE_DATABASE_URI=mongodb://172.31.0.10:27017/parse \
144+
PARSE_SERVER_URL=http://localhost:7100/parse \
145+
docker compose up -d
146+
147+
APP_PORT=7100 PARSE_PHASE=cell-A bash flow.sh bootstrap 240
148+
APP_PORT=7100 PARSE_PHASE=cell-A bash flow.sh record-traffic
149+
```
32150

33-
The `parse-server-mongo` lane in `keploy/integrations` (`.woodpecker/parse-server-mongo.yml`) clones this repo, `cd`s into `parse-server-mongo/`, builds the sample image via `docker compose build`, runs `keploy record -c "docker compose -f docker-compose.yml up"` while `flow.sh` drives the three reproducer calls, then runs `keploy test` for the replay phase. Pass criteria: zero `mongo mock miss`, zero `MongoNetworkError`, zero `ParseError: schema class name does not revalidate` markers in the agent and app logs.
151+
Under keploy record / replay, the lane consumer wraps `docker compose up` with the keploy binary and runs `flow.sh bootstrap` and `flow.sh record-traffic` against the published port.
Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
name: parse-server-mongo
1+
name: ${PARSE_PROJECT:-parse-server-mongo}
22
services:
33
mongo:
44
image: mongo:7
5-
container_name: parse-server-mongo-mongo
5+
container_name: ${PARSE_MONGO_CONTAINER:-parse-server-mongo-mongo}
66
networks:
7-
parse-server-mongo-net:
8-
ipv4_address: 172.30.0.10
7+
parse-net:
8+
ipv4_address: ${PARSE_MONGO_IP:-172.30.0.10}
99
healthcheck:
1010
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
1111
interval: 5s
@@ -15,29 +15,30 @@ services:
1515
parse-server:
1616
build:
1717
context: .
18-
image: parse-server-mongo:local
19-
container_name: parse-server-mongo-app
18+
image: ${PARSE_IMAGE:-parse-server-mongo:local}
19+
container_name: ${PARSE_APP_CONTAINER:-parse-server-mongo-app}
2020
networks:
21-
parse-server-mongo-net:
22-
ipv4_address: 172.30.0.11
21+
parse-net:
22+
ipv4_address: ${PARSE_APP_IP:-172.30.0.11}
2323
depends_on:
2424
mongo:
2525
condition: service_healthy
2626
ports:
27-
- "6100:6100"
27+
- "${PARSE_HOST_PORT:-6100}:${PARSE_CONTAINER_PORT:-6100}"
2828
environment:
29-
PORT: "6100"
30-
PARSE_MOUNT: "/parse"
31-
PARSE_SERVER_APPLICATION_ID: keploy-parse-app
32-
PARSE_SERVER_MASTER_KEY: keploy-parse-master
33-
PARSE_SERVER_MASTER_KEY_IPS: 0.0.0.0/0,::0
34-
PARSE_SERVER_SERVER_URL: http://localhost:6100/parse
35-
PARSE_SERVER_DATABASE_URI: mongodb://172.30.0.10:27017/parse
36-
PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID: "1"
29+
PORT: "${PARSE_CONTAINER_PORT:-6100}"
30+
PARSE_MOUNT: "${PARSE_MOUNT_PATH:-/parse}"
31+
PARSE_SERVER_APPLICATION_ID: ${PARSE_APP_ID:-keploy-parse-app}
32+
PARSE_SERVER_MASTER_KEY: ${PARSE_MASTER_KEY:-keploy-parse-master}
33+
PARSE_SERVER_MASTER_KEY_IPS: ${PARSE_MASTER_KEY_IPS:-0.0.0.0/0,::0}
34+
PARSE_SERVER_SERVER_URL: ${PARSE_SERVER_URL:-http://localhost:6100/parse}
35+
PARSE_SERVER_DATABASE_URI: ${PARSE_DATABASE_URI:-mongodb://172.30.0.10:27017/parse}
36+
PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID: "${PARSE_ALLOW_CUSTOM_OBJECT_ID:-1}"
3737

3838
networks:
39-
parse-server-mongo-net:
39+
parse-net:
40+
name: ${PARSE_NETWORK_NAME:-parse-server-mongo-net}
4041
driver: bridge
4142
ipam:
4243
config:
43-
- subnet: 172.30.0.0/24
44+
- subnet: ${PARSE_NETWORK_SUBNET:-172.30.0.0/24}

0 commit comments

Comments
 (0)