fix(api): treat blank env vars as unset; verify the whole flow in Docker Compose

Found by running the delivery flow against the actual built container rather
than a local dev process.

DEPLOY-02 (Critical) — ADMIN LOGIN RETURNED 500 FOR EVERY USER IN DOCKER.
docker-compose.yml passes each optional variable as ${X:-}, so an unconfigured
one arrives as "" rather than undefined. The code read them with
`process.env.X ?? 'default'`, which only falls back on undefined, so the empty
string reached jsonwebtoken and it threw

  "expiresIn" should be a number of seconds or string representing a timespan

on every sign. Invisible to every existing suite, all of which run against a
local process where the variable is genuinely absent.

Added src/common/env.ts (envOr / envOrNull), which treat blank as unset, and
applied it to every auth, port, upload and SMS read. Three were not cosmetic:
a blank UPLOAD_DIR would have served static files from the FILESYSTEM ROOT, a
blank PORT would have bound a random ephemeral port (Number('') === 0), and a
blank JWT_SECRET would have become the verifying key. Recorded as D25, with a
standing rule to use these readers instead of `??`.

Pinned by src/common/env.spec.ts (9 tests), which reproduces the original throw
against the real jsonwebtoken and asserts the reader can no longer produce it.

DEPLOY-03 (process, documented) — the API image is `COPY release/ ./`, so it
ships a bundle pre-built on the developer machine. `docker compose build` will
happily produce an image from a STALE release/app.js and report success; at the
start of this run that bundle was four days old and the rebuilt container was
missing every route added since. DELIVERY.md's deploy section now leads with
`pnpm build:release`.

New: scripts/test-compose-e2e.cjs — the whole delivery flow against the built
container, over HTTP only, as a black box: a sales expert and an ordinary
customer both sign in with the master code, both place an order, both orders
appear in the admin grid and open there, and the three token boundaries still
hold in the deployed image. 32/32.

Verified: compose e2e 32/32 · jest 54/54 · build 5/5 ·
against the CONTAINER: complete-profile 38/38, report 40/40, expert login 36/36 ·
locally: apps smoke 43/43, admin smoke 43/43, B2B scan 34 ok/0 broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-23 02:27:30 +03:30
co-authored by Claude Opus 5
parent 3f549f1749
commit 24b7a4c395
44 changed files with 11748 additions and 1828 deletions
+32 -1
View File
@@ -102,9 +102,17 @@ pnpm build:release # the production/Docker build
### Deploy (Docker)
```bash
pnpm build:release # ⚠ REQUIRED FIRST — see below
docker compose up -d --build
```
> **⚠ `pnpm build:release` must be run before `docker compose build`.**
> The API image is `COPY release/ ./` — it ships a bundle built on the
> developer machine, not one built inside the image. So `docker compose build`
> will happily produce an image from a **stale** `release/app.js` and report
> success. During this delivery's compose run that bundle was four days old and
> the container was missing every route added since. Tracked as **DEPLOY-03**.
Services: `db` (Postgres 15), `api` (port 3000), `admin-panel`, `user-app`,
`user-app-v2` (each nginx on port 80). The apps expose no host ports —
Dokploy/Traefik routes to them by domain. Uploads persist in the `api_uploads`
@@ -144,7 +152,7 @@ All green on `release/delivery`:
| Suite | Result |
|---|---|
| `pnpm build` (5 workspaces) | ✅ all pass |
| jest (api unit) | **45/45** |
| jest (api unit) | **54/54** |
| `smoke-admin.cjs` — admin flows | **43/43** |
| `smoke-apps.cjs` — customer flows, both apps | **43/43** |
| `scan-admin-b2b.cjs` — every B2B read endpoint | **34 ok, 0 broken** |
@@ -154,9 +162,32 @@ All green on `release/delivery`:
| `test-sales-expert-app-login.cjs` — expert app login (D21) | **36/36** |
| `test-complete-profile.cjs` — «تکمیل پروفایل» | **38/38** |
| `test-report.cjs` — «گزارش» | **40/40** |
| `test-compose-e2e.cjs` — **the whole flow inside Docker Compose** | **32/32** |
| `test-name-trigger.cjs` | **9/9** |
| typecheck (api, admin-panel, user-app, user-app-v2) | ✅ clean |
### Verified inside Docker Compose, not just locally
The whole delivery flow was run against the **built container**, which is what
actually ships: `test-compose-e2e.cjs` signs a **sales expert** and an
**ordinary customer** into the app with the master code, has **both place an
order**, and confirms **both orders appear in the admin panel** and open there.
It also re-checks the three token boundaries in the deployed image. **32/32.**
The three page suites were re-run against the container too — complete-profile
38/38, report 40/40, expert login 36/36.
That run found two deployment problems that no local test could have caught:
- **DEPLOY-02 (fixed):** admin login returned **500 for every user in Docker**.
Compose passes optional variables as `${X:-}`, so an unconfigured one arrives
as `""`, not `undefined`; the code used `?? 'default'`, which only falls back
on `undefined`, so the empty string reached `jsonwebtoken` and it threw on
every sign. Fixed with `src/common/env.ts`, applied to every auth, port,
upload and SMS read, and pinned by a test that reproduces the original throw.
- **DEPLOY-03 (process):** the API image ships a **pre-built** bundle, so
`docker compose build` can silently produce an image from stale code. See the
warning in the deploy section above.
Flows walked end to end: admin login, customer creation, sales-expert
create/edit, product and variant create/edit, order list/detail/status change,
wholesale order placement from the admin panel, cash document creation +