|
1 | 1 | # parse-server-mongo |
2 | 2 |
|
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`. |
4 | 6 |
|
5 | 7 | ## Layout |
6 | 8 |
|
7 | 9 | | File | Role | |
8 | 10 | |---|---| |
9 | | -| `index.js` | 20-line Parse Server bootstrap; reads config from env | |
| 11 | +| `index.js` | 25-line Parse Server bootstrap; reads config from env | |
10 | 12 | | `package.json` | Pins `parse-server@8.2.3` and `express@4.21.2` | |
11 | 13 | | `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: |
14 | 48 |
|
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) |
16 | 61 |
|
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 |
18 | 63 |
|
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: |
20 | 65 |
|
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. |
22 | 120 |
|
23 | 121 | ## Running locally |
24 | 122 |
|
| 123 | +Standalone (no env vars): |
| 124 | + |
25 | 125 | ```bash |
26 | 126 | 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 |
28 | 130 | docker compose down -v |
29 | 131 | ``` |
30 | 132 |
|
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 | +``` |
32 | 150 |
|
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. |
0 commit comments