From b513ea0b3251aedb9d41500ead87e0747b3c0926 Mon Sep 17 00:00:00 2001 From: sweksha-cloud Date: Wed, 30 Sep 2026 07:20:54 -0700 Subject: [PATCH 1/6] fix(deploy): pin prod middleware image to ${TAG} like the other services tag_build_containers.sh builds middlewarenode:${TAG} (since #208), but the prod compose file still ran bare `middlewarenode` (= :latest), so a TAG=vX deploy or rollback left middleware on whatever latest was. Co-Authored-By: Claude Opus 5.5 --- deploy/prod/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/prod/docker-compose.yml b/deploy/prod/docker-compose.yml index 5a21e72a..c4581541 100644 --- a/deploy/prod/docker-compose.yml +++ b/deploy/prod/docker-compose.yml @@ -51,7 +51,7 @@ services: - "8080" middleware: - image: middlewarenode + image: middlewarenode:${TAG:-latest} container_name: middleware environment: - NODE_ENV=production From 944bc17266f9a51348eda86684da5b0ad46756cc Mon Sep 17 00:00:00 2001 From: sweksha-cloud Date: Wed, 30 Sep 2026 07:23:02 -0700 Subject: [PATCH 2/6] fix(env): validate required vars in every non-local NODE_ENV, not just "production" validateEnvironment() in all three servers only ran when NODE_ENV was exactly "production". Any other deployed environment (staging, qa, or a typo like "prod") skipped validation entirely, so middlewareNode would boot with the generated dev INDEX_KEY and only a console.warn. Now only NODE_ENV unset/development/test are treated as local and skipped; everything else must be fully configured. Adds tests for all three services covering both sides of the gate. Follow-up from #208 review. Co-Authored-By: Claude Opus 5.5 --- .../src/tests/validateEnvironment.test.js | 59 ++++++++++++++++++ chessServer/src/validateEnvironment.js | 16 ++++- middlewareNode/config/default.js | 5 +- .../src/config/validateEnvironment.js | 16 ++++- .../tests/validateEnvironment.test.js | 61 +++++++++++++++++++ .../src/tests/validateEnvironment.test.js | 58 ++++++++++++++++++ stockfishServer/src/validateEnvironment.js | 16 ++++- 7 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 chessServer/src/tests/validateEnvironment.test.js create mode 100644 middlewareNode/tests/validateEnvironment.test.js create mode 100644 stockfishServer/src/tests/validateEnvironment.test.js diff --git a/chessServer/src/tests/validateEnvironment.test.js b/chessServer/src/tests/validateEnvironment.test.js new file mode 100644 index 00000000..66e6203c --- /dev/null +++ b/chessServer/src/tests/validateEnvironment.test.js @@ -0,0 +1,59 @@ +const validateEnvironment = require("../validateEnvironment"); + +const ORIGINAL_ENV = process.env; + +describe("validateEnvironment", () => { + let exitSpy; + + beforeEach(() => { + process.env = { ...ORIGINAL_ENV }; + exitSpy = jest.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + jest.spyOn(console, "error").mockImplementation(() => {}); + jest.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + process.env = ORIGINAL_ENV; + jest.restoreAllMocks(); + }); + + const clearRequired = () => { + for (const name of ["MIDDLEWARE_URL", "CORS_ORIGIN", "ALLOWED_ORIGINS"]) { + delete process.env[name]; + } + }; + + it.each([undefined, "", "development", "test"])( + "skips validation when NODE_ENV=%p", + (nodeEnv) => { + clearRequired(); + if (nodeEnv === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = nodeEnv; + + expect(() => validateEnvironment()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + } + ); + + it.each(["production", "staging", "qa", "prod"])( + "exits when required vars are missing and NODE_ENV=%p", + (nodeEnv) => { + clearRequired(); + process.env.NODE_ENV = nodeEnv; + + expect(() => validateEnvironment()).toThrow("process.exit"); + expect(exitSpy).toHaveBeenCalledWith(1); + } + ); + + it("passes in a non-local environment when everything is set", () => { + process.env.NODE_ENV = "staging"; + process.env.MIDDLEWARE_URL = "https://example.com/middleware"; + process.env.CORS_ORIGIN = "https://example.com"; + + expect(() => validateEnvironment()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/chessServer/src/validateEnvironment.js b/chessServer/src/validateEnvironment.js index 14a9bdc7..bf9f3aa1 100644 --- a/chessServer/src/validateEnvironment.js +++ b/chessServer/src/validateEnvironment.js @@ -9,8 +9,18 @@ function hasCorsConfiguration() { ); } +// Local-only environments skip validation. Anything else (production, but +// also staging, qa, or a typo like "prod") is treated as a real deployment +// and must be fully configured, instead of silently booting with dev +// fallbacks such as middlewareNode's generated dev INDEX_KEY. +const LOCAL_ENVIRONMENTS = new Set(["", "development", "test"]); + +function isLocalEnvironment() { + return LOCAL_ENVIRONMENTS.has((process.env.NODE_ENV || "").trim()); +} + function validateEnvironment() { - if (process.env.NODE_ENV !== "production") { + if (isLocalEnvironment()) { return; } @@ -25,12 +35,12 @@ function validateEnvironment() { if (missing.length > 0) { console.error( - `[chessServer] Missing required production environment variables: ${missing.join(", ")}` + `[chessServer] Missing required environment variables for NODE_ENV=${process.env.NODE_ENV}: ${missing.join(", ")}` ); process.exit(1); } - console.log("[chessServer] Required production environment variables validated"); + console.log("[chessServer] Required environment variables validated"); } module.exports = validateEnvironment; diff --git a/middlewareNode/config/default.js b/middlewareNode/config/default.js index e051578b..68f4cf4d 100644 --- a/middlewareNode/config/default.js +++ b/middlewareNode/config/default.js @@ -6,8 +6,9 @@ const path = require("path"); // must have a value" in dev/test when INDEX_KEY isn't set — without checking // a fixed signing key into git history. custom-environment-variables.json // already maps INDEX_KEY onto this key, so any real deployment overrides it; -// validateEnvironment.js still requires INDEX_KEY in production, and runs -// before anything require()s this file, so this never touches disk in prod. +// validateEnvironment.js requires INDEX_KEY in every non-local environment +// (anything but NODE_ENV unset/development/test), and runs before anything +// require()s this file, so this never touches disk in a real deployment. // // Persisted (gitignored) rather than regenerated per boot, so dev JWTs and // password-reset links survive a nodemon restart instead of invalidating on diff --git a/middlewareNode/src/config/validateEnvironment.js b/middlewareNode/src/config/validateEnvironment.js index c6eb8267..99c847f4 100644 --- a/middlewareNode/src/config/validateEnvironment.js +++ b/middlewareNode/src/config/validateEnvironment.js @@ -5,8 +5,18 @@ const REQUIRED_PRODUCTION_VARS = [ "SESSION_SECRET", ]; +// Local-only environments skip validation. Anything else (production, but +// also staging, qa, or a typo like "prod") is treated as a real deployment +// and must be fully configured, instead of silently booting with dev +// fallbacks such as middlewareNode's generated dev INDEX_KEY. +const LOCAL_ENVIRONMENTS = new Set(["", "development", "test"]); + +function isLocalEnvironment() { + return LOCAL_ENVIRONMENTS.has((process.env.NODE_ENV || "").trim()); +} + function validateEnvironment() { - if (process.env.NODE_ENV !== "production") { + if (isLocalEnvironment()) { return; } @@ -17,12 +27,12 @@ function validateEnvironment() { if (missing.length > 0) { console.error( - `[boot] Missing required production environment variables: ${missing.join(", ")}` + `[boot] Missing required environment variables for NODE_ENV=${process.env.NODE_ENV}: ${missing.join(", ")}` ); process.exit(1); } - console.log("[boot] Required production environment variables validated"); + console.log("[boot] Required environment variables validated"); } module.exports = validateEnvironment; diff --git a/middlewareNode/tests/validateEnvironment.test.js b/middlewareNode/tests/validateEnvironment.test.js new file mode 100644 index 00000000..02ee65b2 --- /dev/null +++ b/middlewareNode/tests/validateEnvironment.test.js @@ -0,0 +1,61 @@ +const validateEnvironment = require("../src/config/validateEnvironment"); + +const ORIGINAL_ENV = process.env; + +describe("validateEnvironment", () => { + let exitSpy; + + beforeEach(() => { + process.env = { ...ORIGINAL_ENV }; + exitSpy = jest.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + jest.spyOn(console, "error").mockImplementation(() => {}); + jest.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + process.env = ORIGINAL_ENV; + jest.restoreAllMocks(); + }); + + const clearRequired = () => { + for (const name of ["MONGO_URI", "INDEX_KEY", "CORS_ORIGIN", "SESSION_SECRET"]) { + delete process.env[name]; + } + }; + + it.each([undefined, "", "development", "test"])( + "skips validation when NODE_ENV=%p", + (nodeEnv) => { + clearRequired(); + if (nodeEnv === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = nodeEnv; + + expect(() => validateEnvironment()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + } + ); + + it.each(["production", "staging", "qa", "prod"])( + "exits when required vars are missing and NODE_ENV=%p", + (nodeEnv) => { + clearRequired(); + process.env.NODE_ENV = nodeEnv; + + expect(() => validateEnvironment()).toThrow("process.exit"); + expect(exitSpy).toHaveBeenCalledWith(1); + } + ); + + it("passes in a non-local environment when everything is set", () => { + process.env.NODE_ENV = "staging"; + process.env.MONGO_URI = "mongodb://example/db"; + process.env.INDEX_KEY = "k"; + process.env.CORS_ORIGIN = "https://example.com"; + process.env.SESSION_SECRET = "s"; + + expect(() => validateEnvironment()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/stockfishServer/src/tests/validateEnvironment.test.js b/stockfishServer/src/tests/validateEnvironment.test.js new file mode 100644 index 00000000..6539feaf --- /dev/null +++ b/stockfishServer/src/tests/validateEnvironment.test.js @@ -0,0 +1,58 @@ +const validateEnvironment = require("../validateEnvironment"); + +const ORIGINAL_ENV = process.env; + +describe("validateEnvironment", () => { + let exitSpy; + + beforeEach(() => { + process.env = { ...ORIGINAL_ENV }; + exitSpy = jest.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + jest.spyOn(console, "error").mockImplementation(() => {}); + jest.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + process.env = ORIGINAL_ENV; + jest.restoreAllMocks(); + }); + + const clearRequired = () => { + for (const name of ["CORS_ORIGIN", "ALLOWED_ORIGINS"]) { + delete process.env[name]; + } + }; + + it.each([undefined, "", "development", "test"])( + "skips validation when NODE_ENV=%p", + (nodeEnv) => { + clearRequired(); + if (nodeEnv === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = nodeEnv; + + expect(() => validateEnvironment()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + } + ); + + it.each(["production", "staging", "qa", "prod"])( + "exits when required vars are missing and NODE_ENV=%p", + (nodeEnv) => { + clearRequired(); + process.env.NODE_ENV = nodeEnv; + + expect(() => validateEnvironment()).toThrow("process.exit"); + expect(exitSpy).toHaveBeenCalledWith(1); + } + ); + + it("passes in a non-local environment when everything is set", () => { + process.env.NODE_ENV = "staging"; + process.env.CORS_ORIGIN = "https://example.com"; + + expect(() => validateEnvironment()).not.toThrow(); + expect(exitSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/stockfishServer/src/validateEnvironment.js b/stockfishServer/src/validateEnvironment.js index 4c94408f..ddb5770e 100644 --- a/stockfishServer/src/validateEnvironment.js +++ b/stockfishServer/src/validateEnvironment.js @@ -5,8 +5,18 @@ function hasCorsConfiguration() { ); } +// Local-only environments skip validation. Anything else (production, but +// also staging, qa, or a typo like "prod") is treated as a real deployment +// and must be fully configured, instead of silently booting with dev +// fallbacks such as middlewareNode's generated dev INDEX_KEY. +const LOCAL_ENVIRONMENTS = new Set(["", "development", "test"]); + +function isLocalEnvironment() { + return LOCAL_ENVIRONMENTS.has((process.env.NODE_ENV || "").trim()); +} + function validateEnvironment() { - if (process.env.NODE_ENV !== "production") { + if (isLocalEnvironment()) { return; } @@ -18,12 +28,12 @@ function validateEnvironment() { if (missing.length > 0) { console.error( - `[stockfishServer] Missing required production environment variables: ${missing.join(", ")}` + `[stockfishServer] Missing required environment variables for NODE_ENV=${process.env.NODE_ENV}: ${missing.join(", ")}` ); process.exit(1); } - console.log("[stockfishServer] Required production environment variables validated"); + console.log("[stockfishServer] Required environment variables validated"); } module.exports = validateEnvironment; From 6b47dd430f1bee60f0856fbc0e3f12bd1d5f809a Mon Sep 17 00:00:00 2001 From: sweksha-cloud Date: Wed, 30 Sep 2026 07:26:46 -0700 Subject: [PATCH 3/6] fix(compose): make root docker-compose.yml boot as a local dev stack The middleware service set NODE_ENV=production but passed none of the vars validateEnvironment.js requires (MONGO_URI, INDEX_KEY, CORS_ORIGIN, SESSION_SECRET) - it only mounted config/production.json, which the validator doesn't read - so it exit(1)'d on every start. Switch the stack to NODE_ENV=development, add a mongo service, and pass dev values explicitly. File taken verbatim from sahana/208-azure-cicd (55bcd8ee) so it merges cleanly if that branch lands later. Co-authored-by: SanaBalaji208 Co-Authored-By: Claude Opus 5.5 --- docker-compose.yml | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8dcfd492..4518d4be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,21 @@ - services: + mongo: + image: mongo:6 + container_name: dev-mongo + ports: + - "27017:27017" + volumes: + - dev-mongo-data:/data/db + frontend: - build: ./react-ystemandchess + build: + context: ./react-ystemandchess + args: + REACT_APP_MIDDLEWARE_URL: http://localhost:8000 + REACT_APP_STOCKFISH_SERVER_URL: http://localhost:8080 + REACT_APP_CHESS_SERVER_URL: http://localhost:3001 + REACT_APP_CHESS_CLIENT_URL: http://localhost:3002 + REACT_APP_AGORA_APP_ID: "" container_name: frontend ports: - "3000:3000" @@ -12,6 +26,11 @@ services: container_name: chess-server ports: - "3001:3001" + environment: + - NODE_ENV=development + - PORT=3001 + - MIDDLEWARE_URL=http://middleware:8000 + - CORS_ORIGIN=http://localhost,http://localhost:3000,http://localhost:3002 restart: unless-stopped chess-client: @@ -27,9 +46,16 @@ services: ports: - "8000:8000" environment: - - NODE_ENV=production - volumes: - - ./middlewareNode/config/production.json:/app/config/production.json:ro + - NODE_ENV=development + - PORT=8000 + - MONGO_URI=mongodb://mongo:27017/ystem_dev + - INDEX_KEY=dev-index-key-replace-in-prod + - CORS_ORIGIN=http://localhost,http://localhost:3000 + - SESSION_SECRET=dev-secret-replace-in-prod + - ANALYTICS_RATE_LIMIT_MAX=100 + - LEADERBOARD_RATE_LIMIT_MAX=60 + depends_on: + - mongo restart: unless-stopped stockfish-server: @@ -37,4 +63,11 @@ services: container_name: stockfish-server ports: - "8080:8080" + environment: + - NODE_ENV=development + - PORT=8080 + - CORS_ORIGIN=http://localhost,http://localhost:3000 restart: unless-stopped + +volumes: + dev-mongo-data: From b1a7afdacb13c97bdf0e6d3472c0c4b51b400199 Mon Sep 17 00:00:00 2001 From: sweksha-cloud Date: Fri, 4 Sep 2026 13:23:05 -0700 Subject: [PATCH 4/6] Remove hardcoded seeded test credentials, gate chat.js seeding, add regression tests db.js no longer creates the mentor/student demo accounts with the static, publicly-documented "123123123" password on every non-production boot. It now only seeds non-credential mock data (activity types, lessons, puzzles) and attaches it to an existing demo student if one is present. Demo account creation moves to a new opt-in script, npm run seed:dev, which generates a fresh random password for both accounts on every run, prints it to the console only, and refuses to run in production. README updated to match: instructs running the script instead of listing a static password, and drops the stale "default.json will be provided to contributors" line in favor of the actual .env.example workflow. chat.js's default CoachTemplate/Guardrail seeding is now gated behind the same NODE_ENV=production check db.js already used, instead of running unconditionally on every import. Adds regression tests asserting seeding never fires in production (and does fire outside it, confirming the gate isn't inverted) for both db.js and chat.js. --- README.md | 11 +- middlewareNode/package.json | 3 +- middlewareNode/src/config/db.js | 104 +++------------ middlewareNode/src/routes/chat.js | 13 +- middlewareNode/src/scripts/seedDevAccounts.js | 125 ++++++++++++++++++ .../tests/chatSeeding.regression.test.js | 63 +++++++++ .../tests/dbSeeding.regression.test.js | 77 +++++++++++ 7 files changed, 307 insertions(+), 89 deletions(-) create mode 100644 middlewareNode/src/scripts/seedDevAccounts.js create mode 100644 middlewareNode/tests/chatSeeding.regression.test.js create mode 100644 middlewareNode/tests/dbSeeding.regression.test.js diff --git a/README.md b/README.md index b734cd05..99941dd7 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Before running any services, create the local development environment from the * ./create_dev_envs.sh ``` -A `default.json` file containing environment variables will be provided to contributors and should be placed in `middlewareNode/config`. +Copy `middlewareNode/.env.example` to `middlewareNode/.env` and fill in your local values. A committed `middlewareNode/config/default.js` already provides safe, non-secret defaults, nothing needs to be handed to you separately. ## Running Each Service @@ -98,10 +98,13 @@ npm start The server typically runs on port 8000. You should see `"MongoDB Connected..."` when it starts successfully. -The following credentials can be used for testing mentor and student accounts: +To create local mentor and student demo accounts, run: -* **Mentor:** mentor / 123123123 -* **Student:** student / 123123123 +```bash +npm run seed:dev +``` + +This generates a brand-new random password for both accounts every time it runs and prints them to your terminal. Passwords are never committed or written to a file, and re-running the command rotates both passwords. This only works outside of a production environment. ### Main React Application (Frontend) diff --git a/middlewareNode/package.json b/middlewareNode/package.json index c568ed32..dd9c2693 100644 --- a/middlewareNode/package.json +++ b/middlewareNode/package.json @@ -36,7 +36,8 @@ }, "scripts": { "start": "nodemon src/server.js", - "test": "jest --testPathPatterns=tests/" + "test": "jest --testPathPatterns=tests/", + "seed:dev": "node src/scripts/seedDevAccounts.js" }, "author": "", "license": "ISC", diff --git a/middlewareNode/src/config/db.js b/middlewareNode/src/config/db.js index 57cdbde9..c4a8bc49 100644 --- a/middlewareNode/src/config/db.js +++ b/middlewareNode/src/config/db.js @@ -1,94 +1,34 @@ const mongoose = require("mongoose"); const config = require("config"); -const crypto = require("crypto"); + let db = config.get("mongoURI"); /** - * Seed initial test users into the database + * Seed non-credential mock data into the database: activity types, mock + * activities, mock lessons, mock puzzles. + * + * Deliberately does NOT create the "mentor" / "student" demo accounts. + * Those are credential-bearing and are handled by the opt-in + * `npm run seed:dev` script (src/scripts/seedDevAccounts.js) instead, + * which generates a fresh random password every time it runs rather than + * a static, hardcoded one. See that file for why. + * + * If a demo student account already exists (created via seed:dev), mock + * activity data gets attached to it. If not, that one step is skipped, + * everything else here still seeds normally. */ const seedTestUsers = async () => { try { const db = mongoose.connection.db; - // 1. Seed Users + // Look up the demo student account, if seed:dev has already created + // one, so mock activity data can be attached to it. This function + // never creates user accounts itself. const usersCollection = db.collection("users"); - const count = await usersCollection.countDocuments({ username: { $in: ["mentor", "student"] } }); - let studentId; - - const defaultLessonsCompleted = [ - "Piece Checkmate 1 Basic checkmates", - "Checkmate Pattern 1 Recognize the patterns", - "Checkmate Pattern 2 Recognize the patterns", - "Checkmate Pattern 3 Recognize the patterns", - "Checkmate Pattern 4 Recognize the patterns", - "Piece checkmates 2 Challenging checkmates", - "Knight and Bishop Mate interactive lesson", - "The Pin Pin it to win it", - "The Skewer Yum - Skewers!", - "The Fork Use the fork, Luke", - "Discovered Attacks Including discovered checks", - "Double Check A very powerfull tactic", - "Overloaded Pieces They have too much work", - "Zwischenzug In-between moves", - "X-Ray Attacking through an enemy piece", - "Zugzwang Being forced to move", - "Interference Interpose a piece to great effect", - "Greek Gift Study the greek gift scrifice", - "Deflection Distracting a defender", - "Attraction Lure a piece to bad square", - "Underpromotion Promote - but not to a queen!", - "Desperado A piece is lost, but it can still help", - "Counter Check Respond to a check with a check", - "Undermining Remove the defending piece", - "Clearance Get out of the way!", - "Key Squares Reach the key square", - "Opposition take the opposition", - "7th-Rank Rook Pawn Versus a Queen", - "7th-Rank Rook Pawn And Passive Rook vs Rook", - "Basic Rook Endgames Lucena and Philidor" - ].map(piece => ({ piece, lessonNumber: 0 })); - - if (count === 0) { - console.log("Seeding test users into the database..."); - const mentorPassword = crypto.createHash("sha384").update("123123123").digest("hex"); - const mentor = { - username: "mentor", - password: mentorPassword, - firstName: "Test", - lastName: "Mentor", - email: "mentor@test.com", - role: "mentor", - mentorshipUsername: "student", - accountCreatedAt: new Date().toLocaleString(), - timePlayed: 0 - }; - - const studentPassword = crypto.createHash("sha384").update("123123123").digest("hex"); - const student = { - username: "student", - password: studentPassword, - firstName: "Test", - lastName: "Student", - email: "student@test.com", - role: "student", - mentorshipUsername: "mentor", - accountCreatedAt: new Date().toLocaleString(), - timePlayed: 0, - lessonsCompleted: defaultLessonsCompleted - }; - - const mentorResult = await usersCollection.insertOne(mentor); - const studentResult = await usersCollection.insertOne(student); - studentId = studentResult.insertedId; - console.log("✅ Test users seeded successfully!"); - } else { - const studentDoc = await usersCollection.findOne({ username: "student" }); - if (studentDoc) { - studentId = studentDoc._id; - } - } + const studentDoc = await usersCollection.findOne({ username: "student" }); + const studentId = studentDoc ? studentDoc._id : undefined; - // 2. Seed activityTypes + // 1. Seed activityTypes const activityTypesCollection = db.collection("activityTypes"); const activityTypesCount = await activityTypesCollection.countDocuments({}); if (activityTypesCount === 0) { @@ -104,7 +44,7 @@ const seedTestUsers = async () => { console.log("✅ Activity types seeded successfully!"); } - // 3. Seed activities for student + // 2. Seed activities for student if (studentId) { const activitiesCollection = db.collection("activities"); const activitiesCount = await activitiesCollection.countDocuments({ userId: studentId }); @@ -125,7 +65,7 @@ const seedTestUsers = async () => { } } - // 4. Seed newLessons + // 3. Seed newLessons const lessonsCollection = db.collection("newLessons"); const lessonsCount = await lessonsCollection.countDocuments({}); if (lessonsCount === 0) { @@ -186,7 +126,7 @@ const seedTestUsers = async () => { console.log("✅ Mock lessons seeded successfully!"); } - // 5. Seed puzzles + // 4. Seed puzzles const puzzlesCollection = db.collection("puzzles"); const puzzlesCount = await puzzlesCollection.countDocuments({}); if (puzzlesCount === 0) { diff --git a/middlewareNode/src/routes/chat.js b/middlewareNode/src/routes/chat.js index 9d8618cf..76bd7c9f 100644 --- a/middlewareNode/src/routes/chat.js +++ b/middlewareNode/src/routes/chat.js @@ -13,6 +13,11 @@ const passport = require('passport'); const Guardrail = require('../models/Guardrail'); const requireAuth = require('../middleware/requireAuth'); +// Same fail-loud-in-production posture as config/db.js's seedTestUsers — +// config-template seeding, not credential-bearing, but still an +// unconditional side effect on import that shouldn't run in production. +const IS_PRODUCTION = process.env.NODE_ENV === 'production'; + // Ensure logs directory exists const LOGS_DIR = path.join(__dirname, '../../logs'); fs.mkdirSync(LOGS_DIR, { recursive: true }); @@ -97,7 +102,9 @@ const seedDefaultTemplates = async () => { console.error('Error seeding CoachTemplates:', error.message); } }; -seedDefaultTemplates(); +if (!IS_PRODUCTION) { + seedDefaultTemplates(); +} // Seeding default guardrails in MongoDB on start const seedDefaultGuardrail = async () => { @@ -113,7 +120,9 @@ const seedDefaultGuardrail = async () => { console.error('Error seeding Guardrails:', error.message); } }; -seedDefaultGuardrail(); +if (!IS_PRODUCTION) { + seedDefaultGuardrail(); +} // Middleware to authorize Tutors or Admins const authorizeTutorAdmin = (req, res, next) => { diff --git a/middlewareNode/src/scripts/seedDevAccounts.js b/middlewareNode/src/scripts/seedDevAccounts.js new file mode 100644 index 00000000..b43ec58f --- /dev/null +++ b/middlewareNode/src/scripts/seedDevAccounts.js @@ -0,0 +1,125 @@ +/** + * Dev-Only Demo Account Seeder + * + * Creates (or resets) the "mentor" and "student" demo accounts used for + * manual local development, so a developer can open the app and click + * around as either role without running the signup flow first. + * + * This replaces the old approach of a fixed, hardcoded password + * (`123123123`) that was created automatically on every server start and + * published in the README. That's what let it end up guessable and public + * for years. This script is the opposite on every axis: + * + * - Opt-in only. Nothing calls this automatically. Run it yourself: + * npm run seed:dev + * - Every run generates a brand-new random password for both accounts, + * whether the account already exists or not. There is no static value + * to leak, and an old password stops working the moment you rotate it + * by running this again. + * - The password is printed to the terminal only. It is never written to + * a file, never logged anywhere persistent, and never committed. + * - Refuses to run at all if NODE_ENV=production, matching the same + * fail-loud posture already used in src/config/validateEnvironment.js. + * + * Usage: + * npm run seed:dev + * (or, directly: node src/scripts/seedDevAccounts.js) + * + * Requires MONGO_URI (or a local config/default.json) pointing at a real, + * disposable development database. Never point this at production data. + */ + +require("dotenv").config(); +const mongoose = require("mongoose"); +const config = require("config"); +const crypto = require("crypto"); +const Users = require("../models/users"); + +if (process.env.NODE_ENV === "production") { + console.error( + "Refusing to seed demo accounts in production. This script is for local development only." + ); + process.exit(1); +} + +/** + * 16 bytes of randomness, base64url-encoded so it's safe to read off a + * terminal or type back in by hand if needed. + */ +function generatePassword() { + return crypto.randomBytes(16).toString("base64url"); +} + +/** + * Matches the hashing scheme every login/signup route already uses + * (routes/auth.js, routes/users.js). This script's job is to stay + * consistent with that scheme, not to change it. + */ +function hashPassword(plaintext) { + return crypto.createHash("sha384").update(plaintext).digest("hex"); +} + +async function upsertDemoAccount({ username, role, mentorshipUsername, firstName, lastName, email }) { + const plaintextPassword = generatePassword(); + const hashedPassword = hashPassword(plaintextPassword); + + await Users.findOneAndUpdate( + { username }, + { + $set: { + password: hashedPassword, + role, + mentorshipUsername, + firstName, + lastName, + email, + }, + $setOnInsert: { + accountCreatedAt: new Date().toLocaleString(), + timePlayed: 0, + }, + }, + { upsert: true, new: true, setDefaultsOnInsert: true } + ); + + return plaintextPassword; +} + +async function run() { + await mongoose.connect(config.get("mongoURI")); + console.log("Connected to MongoDB"); + + const mentorPassword = await upsertDemoAccount({ + username: "mentor", + role: "mentor", + mentorshipUsername: "student", + firstName: "Demo", + lastName: "Mentor", + email: "demo-mentor@ystemandchess.local", + }); + + const studentPassword = await upsertDemoAccount({ + username: "student", + role: "student", + mentorshipUsername: "mentor", + firstName: "Demo", + lastName: "Student", + email: "demo-student@ystemandchess.local", + }); + + await mongoose.disconnect(); + + console.log(""); + console.log("Demo accounts ready. These passwords are freshly generated for this run only,"); + console.log("they are shown here and nowhere else:"); + console.log(""); + console.log(` mentor / ${mentorPassword}`); + console.log(` student / ${studentPassword}`); + console.log(""); + console.log("Run `npm run seed:dev` again any time to rotate both passwords."); +} + +run().catch((err) => { + console.error("Seeding demo accounts failed:", err.message); + process.exit(1); +}); diff --git a/middlewareNode/tests/chatSeeding.regression.test.js b/middlewareNode/tests/chatSeeding.regression.test.js new file mode 100644 index 00000000..1e2d3a37 --- /dev/null +++ b/middlewareNode/tests/chatSeeding.regression.test.js @@ -0,0 +1,63 @@ +/** + * Regression test — chat.js's default CoachTemplate/Guardrail seeding + * must never run when NODE_ENV=production. Issue 5, Phase 2/6 (same gate + * pattern as db.js's seedTestUsers, applied here for consistency). + */ + +jest.mock("../src/models/ChatSession"); +jest.mock("../src/models/ChatMessage"); +jest.mock("../src/models/CoachTemplate"); +jest.mock("../src/models/Guardrail"); +// Unrelated to seeding — this test resetModules()'s chat.js twice, and the +// real chatService kicks off a genuine (fire-and-forget) NLP training run +// on each require that can otherwise still be in flight after the test +// file finishes, producing spurious "log after tests are done" noise. +jest.mock("../src/utils/chatService"); +jest.mock("passport", () => ({ + authenticate: jest.fn(() => (req, res, next) => next()), +})); + +const ORIGINAL_NODE_ENV = process.env.NODE_ENV; + +afterEach(() => { + process.env.NODE_ENV = ORIGINAL_NODE_ENV; + jest.resetModules(); + jest.clearAllMocks(); +}); + +// seedDefaultTemplates/seedDefaultGuardrail are fire-and-forget promises +// kicked off at module load — flush the microtask queue before asserting +// so we're not checking before an (incorrectly) fired call would land. +const flush = () => new Promise((resolve) => setImmediate(resolve)); + +describe("chat.js seeding — production gate", () => { + test("does NOT seed default templates or guardrails when NODE_ENV=production", async () => { + process.env.NODE_ENV = "production"; + jest.resetModules(); + + const CoachTemplate = require("../src/models/CoachTemplate"); + const Guardrail = require("../src/models/Guardrail"); + + require("../src/routes/chat"); + await flush(); + await flush(); + + expect(CoachTemplate.findOneAndUpdate).not.toHaveBeenCalled(); + expect(Guardrail.countDocuments).not.toHaveBeenCalled(); + }); + + test("DOES seed default templates and guardrails outside production (gate isn't inverted)", async () => { + process.env.NODE_ENV = "test"; + jest.resetModules(); + + const CoachTemplate = require("../src/models/CoachTemplate"); + const Guardrail = require("../src/models/Guardrail"); + + require("../src/routes/chat"); + await flush(); + await flush(); + + expect(CoachTemplate.findOneAndUpdate).toHaveBeenCalled(); + expect(Guardrail.countDocuments).toHaveBeenCalled(); + }); +}); diff --git a/middlewareNode/tests/dbSeeding.regression.test.js b/middlewareNode/tests/dbSeeding.regression.test.js new file mode 100644 index 00000000..27ba6afe --- /dev/null +++ b/middlewareNode/tests/dbSeeding.regression.test.js @@ -0,0 +1,77 @@ +/** + * Regression test — db.js's mock-data seeding (seedTestUsers) must never + * run when NODE_ENV=production. Issue 5, Phase 6. + * + * mongoose is fully mocked so this runs without a real database; the only + * thing under test is whether the seeding-specific collections get + * touched at all, not what gets written to them. + */ + +jest.mock("mongoose", () => { + const collectionsRequested = []; + const fakeCollection = { + createIndex: jest.fn().mockResolvedValue(undefined), + countDocuments: jest.fn().mockResolvedValue(0), + // Simulates a demo student account already existing (e.g. created via + // seed:dev), so the conditional activities-seeding branch is actually + // exercised by these tests too, not just the unconditional collections. + findOne: jest.fn().mockResolvedValue({ _id: "mock-student-id" }), + insertOne: jest.fn().mockResolvedValue({ insertedId: "mockid" }), + insertMany: jest.fn().mockResolvedValue({}), + }; + // seedTestUsers reaches collections via mongoose.connection.db.collection(...) + const dbCollectionSpy = jest.fn((name) => { + collectionsRequested.push(name); + return fakeCollection; + }); + return { + connect: jest.fn().mockResolvedValue(undefined), + connection: { + db: { collection: dbCollectionSpy }, + // ensureIndexes reaches collections via mongoose.connection.collection(...) + // directly — kept separate so its index-creation calls (which run + // regardless of environment) don't pollute the seeding assertions below. + collection: jest.fn(() => fakeCollection), + }, + __collectionsRequested: collectionsRequested, + }; +}); + +const ORIGINAL_NODE_ENV = process.env.NODE_ENV; + +afterEach(() => { + process.env.NODE_ENV = ORIGINAL_NODE_ENV; + jest.resetModules(); +}); + +const SEED_ONLY_COLLECTIONS = ["activityTypes", "activities", "newLessons", "puzzles"]; + +describe("db.js seeding — production gate", () => { + test("does NOT seed mock data when NODE_ENV=production", async () => { + process.env.NODE_ENV = "production"; + jest.resetModules(); + + const mongoose = require("mongoose"); + const connectDB = require("../src/config/db"); + await connectDB(); + + const requested = mongoose.__collectionsRequested; + for (const name of SEED_ONLY_COLLECTIONS) { + expect(requested).not.toContain(name); + } + }); + + test("DOES seed mock data outside production (gate isn't inverted)", async () => { + process.env.NODE_ENV = "test"; + jest.resetModules(); + + const mongoose = require("mongoose"); + const connectDB = require("../src/config/db"); + await connectDB(); + + const requested = mongoose.__collectionsRequested; + for (const name of SEED_ONLY_COLLECTIONS) { + expect(requested).toContain(name); + } + }); +}); From 3ab9a4b47284c673b1b02afa3f944c560b45dd9e Mon Sep 17 00:00:00 2001 From: sweksha-cloud Date: Wed, 30 Sep 2026 14:42:53 -0700 Subject: [PATCH 5/6] Remove unused JWT_SECRET config JWT_SECRET / jwtSecret was declared in .env.example, default.js, and custom-environment-variables.json but never read by any code. Every JWT sign/verify call (passport.js, auth.js, users.js, changePasswordTemplate.js, utils/middleware.js) uses indexKey / INDEX_KEY, which is the real signing secret and is already required in production by validateEnvironment.js. Keeping the dead key in the template implied it did something, which is the kind of config confusion issues 6/7 exist to remove. --- middlewareNode/.env.example | 1 - middlewareNode/config/custom-environment-variables.json | 1 - middlewareNode/config/default.js | 1 - 3 files changed, 3 deletions(-) diff --git a/middlewareNode/.env.example b/middlewareNode/.env.example index 6bd6f59a..c5ed04a1 100644 --- a/middlewareNode/.env.example +++ b/middlewareNode/.env.example @@ -60,5 +60,4 @@ PVP_WEIGHT_LOSS=0 ADMIN_USERNAME= # Present in config mapping but not currently consumed by middleware source -JWT_SECRET= EMAIL_PASS= diff --git a/middlewareNode/config/custom-environment-variables.json b/middlewareNode/config/custom-environment-variables.json index 73430b29..a4fa8ee1 100644 --- a/middlewareNode/config/custom-environment-variables.json +++ b/middlewareNode/config/custom-environment-variables.json @@ -1,6 +1,5 @@ { "mongoURI": "MONGO_URI", - "jwtSecret": "JWT_SECRET", "indexKey": "INDEX_KEY", "corsOptions": { "origin": "CORS_ORIGIN" }, diff --git a/middlewareNode/config/default.js b/middlewareNode/config/default.js index 68f4cf4d..7c9fe68f 100644 --- a/middlewareNode/config/default.js +++ b/middlewareNode/config/default.js @@ -45,7 +45,6 @@ if (!process.env.INDEX_KEY) { module.exports = { mongoURI: "", - jwtSecret: "", indexKey: devIndexKey, corsOptions: { From 0ae75654107fcf39df09b1f27800516615cabf58 Mon Sep 17 00:00:00 2001 From: sweksha-cloud Date: Wed, 30 Sep 2026 14:49:35 -0700 Subject: [PATCH 6/6] fix(env): gate middleware seeding and DB fallback on isLocalEnvironment() db.js, chat.js, seedDevAccounts.js, and server.js's session-secret fallback still checked NODE_ENV === "production" exactly, so on staging, qa, or a typo like "prod" middleware would still seed mock data (and chat templates) into a real database, use the 1s dev connection timeout, fall back to an in-memory MongoDB on a connection failure, and allow the demo-account seed script to run. All four now use the same isLocalEnvironment() check validateEnvironment.js uses (only unset/development/test count as local), exported from that module so there is one definition of "local". Regression tests now cover production, staging, qa, and prod for both the db.js and chat.js seeding gates, and assert the connection timeout. --- middlewareNode/src/config/db.js | 16 ++++++++++------ middlewareNode/src/config/validateEnvironment.js | 1 + middlewareNode/src/routes/chat.js | 13 +++++++------ middlewareNode/src/scripts/seedDevAccounts.js | 10 ++++++---- middlewareNode/src/server.js | 2 +- .../tests/chatSeeding.regression.test.js | 13 +++++++------ .../tests/dbSeeding.regression.test.js | 15 +++++++++------ 7 files changed, 41 insertions(+), 29 deletions(-) diff --git a/middlewareNode/src/config/db.js b/middlewareNode/src/config/db.js index c4a8bc49..4445b927 100644 --- a/middlewareNode/src/config/db.js +++ b/middlewareNode/src/config/db.js @@ -1,5 +1,6 @@ const mongoose = require("mongoose"); const config = require("config"); +const { isLocalEnvironment } = require("./validateEnvironment"); let db = config.get("mongoURI"); @@ -189,7 +190,10 @@ async function ensureIndexes() { } } -const IS_PRODUCTION = process.env.NODE_ENV === "production"; +// Any non-local NODE_ENV (production, but also staging, qa, or a typo like +// "prod") is a real deployment: no mock-data seeding, no in-memory fallback, +// and the longer connection timeout. Same rule validateEnvironment.js uses. +const IS_DEPLOYED = !isLocalEnvironment(); const connectDB = async () => { try { @@ -210,24 +214,24 @@ const connectDB = async () => { await mongoose.connect(db, { useNewUrlParser: true, useUnifiedTopology: true, - // Production has no fallback (a failure here calls process.exit(1)), + // Deployed environments have no fallback (a failure here calls process.exit(1)), // so it gets a longer timeout to ride out a network blip or an Atlas // cold-start during deploy. Dev/test fail fast to the in-memory // fallback below. - serverSelectionTimeoutMS: IS_PRODUCTION ? 10000 : 1000, + serverSelectionTimeoutMS: IS_DEPLOYED ? 10000 : 1000, }); console.log("MongoDB Connected..."); await ensureIndexes(); - if (!IS_PRODUCTION) { + if (!IS_DEPLOYED) { await seedTestUsers(); } } catch (err) { console.warn(`Connection to configured MongoDB failed: ${err.message}`); - if (IS_PRODUCTION) { + if (IS_DEPLOYED) { console.error( - "Refusing to fall back to in-memory MongoDB in production. Exiting." + `Refusing to fall back to in-memory MongoDB with NODE_ENV=${process.env.NODE_ENV}. Exiting.` ); process.exit(1); } diff --git a/middlewareNode/src/config/validateEnvironment.js b/middlewareNode/src/config/validateEnvironment.js index 99c847f4..bcc2321a 100644 --- a/middlewareNode/src/config/validateEnvironment.js +++ b/middlewareNode/src/config/validateEnvironment.js @@ -36,3 +36,4 @@ function validateEnvironment() { } module.exports = validateEnvironment; +module.exports.isLocalEnvironment = isLocalEnvironment; diff --git a/middlewareNode/src/routes/chat.js b/middlewareNode/src/routes/chat.js index 76bd7c9f..9df28487 100644 --- a/middlewareNode/src/routes/chat.js +++ b/middlewareNode/src/routes/chat.js @@ -12,11 +12,12 @@ const { detectCrisis, getCrisisResponse } = require('../utils/guardrails'); const passport = require('passport'); const Guardrail = require('../models/Guardrail'); const requireAuth = require('../middleware/requireAuth'); +const { isLocalEnvironment } = require('../config/validateEnvironment'); -// Same fail-loud-in-production posture as config/db.js's seedTestUsers — -// config-template seeding, not credential-bearing, but still an -// unconditional side effect on import that shouldn't run in production. -const IS_PRODUCTION = process.env.NODE_ENV === 'production'; +// Same gate as config/db.js's seedTestUsers: config-template seeding, not +// credential-bearing, but still a side effect on import that should only +// run locally, not in production, staging, qa, or any other deployment. +const IS_DEPLOYED = !isLocalEnvironment(); // Ensure logs directory exists const LOGS_DIR = path.join(__dirname, '../../logs'); @@ -102,7 +103,7 @@ const seedDefaultTemplates = async () => { console.error('Error seeding CoachTemplates:', error.message); } }; -if (!IS_PRODUCTION) { +if (!IS_DEPLOYED) { seedDefaultTemplates(); } @@ -120,7 +121,7 @@ const seedDefaultGuardrail = async () => { console.error('Error seeding Guardrails:', error.message); } }; -if (!IS_PRODUCTION) { +if (!IS_DEPLOYED) { seedDefaultGuardrail(); } diff --git a/middlewareNode/src/scripts/seedDevAccounts.js b/middlewareNode/src/scripts/seedDevAccounts.js index b43ec58f..db514fb0 100644 --- a/middlewareNode/src/scripts/seedDevAccounts.js +++ b/middlewareNode/src/scripts/seedDevAccounts.js @@ -18,8 +18,9 @@ * by running this again. * - The password is printed to the terminal only. It is never written to * a file, never logged anywhere persistent, and never committed. - * - Refuses to run at all if NODE_ENV=production, matching the same - * fail-loud posture already used in src/config/validateEnvironment.js. + * - Refuses to run at all outside local development (any NODE_ENV other + * than unset/development/test), using the same isLocalEnvironment() + * check as src/config/validateEnvironment.js. * * Usage: * npm run seed:dev @@ -34,10 +35,11 @@ const mongoose = require("mongoose"); const config = require("config"); const crypto = require("crypto"); const Users = require("../models/users"); +const { isLocalEnvironment } = require("../config/validateEnvironment"); -if (process.env.NODE_ENV === "production") { +if (!isLocalEnvironment()) { console.error( - "Refusing to seed demo accounts in production. This script is for local development only." + `Refusing to seed demo accounts with NODE_ENV=${process.env.NODE_ENV}. This script is for local development only.` ); process.exit(1); } diff --git a/middlewareNode/src/server.js b/middlewareNode/src/server.js index 5167de20..8d0cc178 100644 --- a/middlewareNode/src/server.js +++ b/middlewareNode/src/server.js @@ -87,7 +87,7 @@ app.use( session({ secret: process.env.SESSION_SECRET || - (process.env.NODE_ENV !== "production" + (validateEnvironment.isLocalEnvironment() ? "dev-secret-change-in-prod" : undefined), resave: false, diff --git a/middlewareNode/tests/chatSeeding.regression.test.js b/middlewareNode/tests/chatSeeding.regression.test.js index 1e2d3a37..0d8b4a1c 100644 --- a/middlewareNode/tests/chatSeeding.regression.test.js +++ b/middlewareNode/tests/chatSeeding.regression.test.js @@ -1,6 +1,7 @@ /** * Regression test — chat.js's default CoachTemplate/Guardrail seeding - * must never run when NODE_ENV=production. Issue 5, Phase 2/6 (same gate + * must never run in a deployed environment (production, staging, qa, or + * any other non-local NODE_ENV). Issue 5, Phase 2/6 (same gate * pattern as db.js's seedTestUsers, applied here for consistency). */ @@ -30,9 +31,9 @@ afterEach(() => { // so we're not checking before an (incorrectly) fired call would land. const flush = () => new Promise((resolve) => setImmediate(resolve)); -describe("chat.js seeding — production gate", () => { - test("does NOT seed default templates or guardrails when NODE_ENV=production", async () => { - process.env.NODE_ENV = "production"; +describe("chat.js seeding — deployed-environment gate", () => { + test.each(["production", "staging", "qa", "prod"])("does NOT seed default templates or guardrails when NODE_ENV=%s", async (env) => { + process.env.NODE_ENV = env; jest.resetModules(); const CoachTemplate = require("../src/models/CoachTemplate"); @@ -46,8 +47,8 @@ describe("chat.js seeding — production gate", () => { expect(Guardrail.countDocuments).not.toHaveBeenCalled(); }); - test("DOES seed default templates and guardrails outside production (gate isn't inverted)", async () => { - process.env.NODE_ENV = "test"; + test.each(["test", "development"])("DOES seed default templates and guardrails when NODE_ENV=%s (gate isn't inverted)", async (env) => { + process.env.NODE_ENV = env; jest.resetModules(); const CoachTemplate = require("../src/models/CoachTemplate"); diff --git a/middlewareNode/tests/dbSeeding.regression.test.js b/middlewareNode/tests/dbSeeding.regression.test.js index 27ba6afe..8902e6a7 100644 --- a/middlewareNode/tests/dbSeeding.regression.test.js +++ b/middlewareNode/tests/dbSeeding.regression.test.js @@ -1,6 +1,7 @@ /** * Regression test — db.js's mock-data seeding (seedTestUsers) must never - * run when NODE_ENV=production. Issue 5, Phase 6. + * run in a deployed environment (production, staging, qa, or any other + * non-local NODE_ENV). Issue 5, Phase 6. * * mongoose is fully mocked so this runs without a real database; the only * thing under test is whether the seeding-specific collections get @@ -46,9 +47,9 @@ afterEach(() => { const SEED_ONLY_COLLECTIONS = ["activityTypes", "activities", "newLessons", "puzzles"]; -describe("db.js seeding — production gate", () => { - test("does NOT seed mock data when NODE_ENV=production", async () => { - process.env.NODE_ENV = "production"; +describe("db.js seeding — deployed-environment gate", () => { + test.each(["production", "staging", "qa", "prod"])("does NOT seed mock data when NODE_ENV=%s", async (env) => { + process.env.NODE_ENV = env; jest.resetModules(); const mongoose = require("mongoose"); @@ -59,10 +60,11 @@ describe("db.js seeding — production gate", () => { for (const name of SEED_ONLY_COLLECTIONS) { expect(requested).not.toContain(name); } + expect(mongoose.connect.mock.calls[0][1].serverSelectionTimeoutMS).toBe(10000); }); - test("DOES seed mock data outside production (gate isn't inverted)", async () => { - process.env.NODE_ENV = "test"; + test.each(["test", "development"])("DOES seed mock data when NODE_ENV=%s (gate isn't inverted)", async (env) => { + process.env.NODE_ENV = env; jest.resetModules(); const mongoose = require("mongoose"); @@ -73,5 +75,6 @@ describe("db.js seeding — production gate", () => { for (const name of SEED_ONLY_COLLECTIONS) { expect(requested).toContain(name); } + expect(mongoose.connect.mock.calls[0][1].serverSelectionTimeoutMS).toBe(1000); }); });