Skip to content

Commit 933a619

Browse files
tariqksolimandevin-ai-integration[bot]github-actions[bot]
authored
feat: add extra e2e test safety improvements (production fail-safe, test DB credentials, AI agent rules) (#951)
* feat: add production fail-safe checks, test DB credential separation, and AI agent safety rules - Add NODE_ENV=production and DATABASE_URL production-indicator checks to tests/global-setup.js and tests/test-db-clean.js - Support DB_USER_TEST / DB_PASS_TEST env vars for least-privilege test database credential separation - Add Database Safety Rules section to AGENTS.md and AI-GETTING-STARTED.md - Create .cursorrules with database safety guidelines for AI agents Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.26-20260427 [version bump] * remove .cursorrules — not used Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: handle promise rejection from clean() safety checks Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove DATABASE_URL check, update error wording, require explicit test creds in test-db-clean - Remove DATABASE_URL production check (no such ENV exists) - Change 'destructive test operations' to 'test operations' in error message - test-db-clean.js now requires DB_USER_TEST/DB_PASS_TEST with no fallback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: add mmgis-stac-test DB isolation and STAC_DB_NAME env var - Make STAC DB name configurable via STAC_DB_NAME in API/connection.js and scripts/init-db.js (defaults to 'mmgis-stac') - global-setup.js creates mmgis-stac-test when STAC services are enabled and passes STAC_DB_NAME to the test server - Adjacent server .env files rewritten to use mmgis-stac-test - test-db-clean.js drops mmgis-stac-test alongside mmgis-test - Add DB_USER_TEST, DB_PASS_TEST, STAC_DB_NAME to sample.env and ENVs.md - Update safety rules in AGENTS.md and AI-GETTING-STARTED.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: comment out empty env vars in sample.env, fix STAC cleanup independence - Comment out DB_USER_TEST, DB_PASS_TEST, STAC_DB_NAME in sample.env to prevent dotenv from setting them to empty/whitespace values - Fix early return in test-db-clean.js so mmgis-stac-test cleanup runs independently of whether mmgis-test exists Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: move test env vars to Optional Variables section in ENVs.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: require DB_USER_TEST/DB_PASS_TEST in global-setup.js (no fallback) - Remove fallback to DB_USER/DB_PASS in global-setup.js credential resolution - Add DB_USER_TEST/DB_PASS_TEST to CI workflow .env setup - Update ENVs.md to reflect these are now required for tests Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove STAC_DB_NAME env var, hardcode mmgis-stac and mmgis-stac-test - Revert API/connection.js to hardcoded 'mmgis-stac' - Revert scripts/init-db.js to hardcoded 'mmgis-stac' - Test infrastructure uses hardcoded 'mmgis-stac-test' constant - Remove STAC_DB_NAME from sample.env and ENVs.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: update sample.env comment — test creds required in both files, no fallback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: add windowsHide to suppress console windows on Windows Prevents execSync and spawn calls in global-setup.js from flashing empty terminal windows on Windows machines. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent ea02627 commit 933a619

10 files changed

Lines changed: 175 additions & 28 deletions

File tree

.github/workflows/playwright-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ jobs:
6464
echo "DB_NAME=mmgis-test" >> .env
6565
echo "DB_USER=test_user" >> .env
6666
echo "DB_PASS=test_password" >> .env
67+
echo "DB_USER_TEST=test_user" >> .env
68+
echo "DB_PASS_TEST=test_password" >> .env
6769
echo "ENABLE_MMGIS_WEBSOCKETS=false" >> .env
6870
echo "ENABLE_CONFIG_WEBSOCKETS=false" >> .env
6971
echo "HIDE_CONFIG=false" >> .env

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,18 @@ ws.on("message", function (message) {
717717
- Review `webpack.config.js` for misconfigurations
718718
- Check Node.js version (requires 20+)
719719

720+
## Database Safety Rules for AI Agents
721+
722+
When writing or modifying code that interacts with the database, AI agents MUST follow these rules:
723+
724+
1. **NEVER use `DROP DATABASE` in application code.** The only place `DROP DATABASE` is permitted is in `tests/test-db-clean.js`, and only against the hardcoded `mmgis-test` and `mmgis-stac-test` databases.
725+
2. **NEVER use `DROP TABLE` or `TRUNCATE TABLE` without proper authorization checks** and input sanitization via `Utils.forceAlphaNumUnder()`.
726+
3. **NEVER hardcode production database names, hosts, or credentials** in test files or scripts.
727+
4. **ALWAYS use the dedicated test databases** (`mmgis-test` and `mmgis-stac-test`) for any test-related database operations. Never modify the test database name constants.
728+
5. **ALWAYS use `DB_USER_TEST` / `DB_PASS_TEST`** environment variables for test database credentials when available, to maintain least-privilege separation.
729+
6. **NEVER remove or weaken** the `NODE_ENV === 'production'` safety checks in test setup files (`tests/global-setup.js`, `tests/test-db-clean.js`).
730+
7. When writing database-related tests, **never use destructive commands** like `DROP` or `TRUNCATE` on the main schema. Always target the `mmgis-test` (or `mmgis-stac-test` for STAC) database and implement environment safety checks.
731+
720732
## References
721733

722734
- **Official Documentation**: https://nasa-ammos.github.io/MMGIS/

AI-GETTING-STARTED.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,17 @@ All options are documented in `sample.env`. The critical ones:
144144
- Test files are located in `tests/e2e/`.
145145
- The accessibility test (`tests/e2e/accessibility.spec.js`) scans the landing page and map interface for WCAG 2.1 AA violations.
146146
- Use `npm run test:headed` to see tests run in a visible browser, or `npm run test:debug` to step through tests interactively.
147+
148+
---
149+
150+
## 9. Database Safety Rules for AI Agents
151+
152+
When writing or modifying code that interacts with the database, AI agents MUST follow these rules:
153+
154+
1. **NEVER use `DROP DATABASE` in application code.** The only place `DROP DATABASE` is permitted is in `tests/test-db-clean.js`, and only against the hardcoded `mmgis-test` and `mmgis-stac-test` databases.
155+
2. **NEVER use `DROP TABLE` or `TRUNCATE TABLE` without proper authorization checks** and input sanitization via `Utils.forceAlphaNumUnder()`.
156+
3. **NEVER hardcode production database names, hosts, or credentials** in test files or scripts.
157+
4. **ALWAYS use the dedicated test databases** (`mmgis-test` and `mmgis-stac-test`) for any test-related database operations. Never modify the test database name constants.
158+
5. **ALWAYS use `DB_USER_TEST` / `DB_PASS_TEST`** environment variables for test database credentials when available, to maintain least-privilege separation.
159+
6. **NEVER remove or weaken** the `NODE_ENV === 'production'` safety checks in test setup files (`tests/global-setup.js`, `tests/test-db-clean.js`).
160+
7. When writing database-related tests, **never use destructive commands** like `DROP` or `TRUNCATE` on the main schema. Always target the `mmgis-test` (or `mmgis-stac-test` for STAC) database and implement environment safety checks.

configure/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "configure",
3-
"version": "4.3.25-20260423",
3+
"version": "4.3.26-20260427",
44
"homepage": "./configure/build",
55
"private": true,
66
"dependencies": {

docs/pages/Setup/ENVs/ENVs.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ If `DB_SSL=true` and if needed, the path to a certificate for ssl | string | def
105105

106106
Alternatively, if `DB_SSL=true` and if needed, a base64 encoded certificate for ssl. `DB_SSL_CERT_BASE64` will take priority over `DB_SSL_CERT` | string | default `null`
107107

108+
#### `DB_USER_TEST=`
109+
110+
Test-specific database user. Required by test infrastructure (`tests/global-setup.js`, `tests/test-db-clean.js`) for least-privilege separation. No fallback — tests will not run without this | string | default `null`
111+
112+
#### `DB_PASS_TEST=`
113+
114+
Test-specific database password. Required by test infrastructure for least-privilege separation. No fallback — tests will not run without this | string | default `null`
115+
108116
#### `AUTH_LOCAL_ALLOW_SIGNUP=`
109117

110118
If AUTH=local and set to true, this allows all guests to the site to create user accounts otherwise, they just see a login page with no signup section | boolean | default `false`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mmgis",
3-
"version": "4.3.25-20260423",
3+
"version": "4.3.26-20260427",
44
"description": "A web-based mapping and localization solution for science operation on planetary missions.",
55
"homepage": "build",
66
"repository": {

sample.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ DB_PORT=5432
121121
DB_NAME=name
122122
DB_USER=user
123123
DB_PASS=password
124+
# Test database credentials for least-privilege separation.
125+
# Required by both tests/global-setup.js and tests/test-db-clean.js (no fallback).
126+
# DB_USER_TEST=
127+
# DB_PASS_TEST=
124128
# Max number connections in the database's pool. CPUs * 4 is a good number. Default is 10
125129
DB_POOL_MAX=
126130
# How many milliseconds until a DB connection times out. Default is 30000 (30 sec)

scripts/init-db.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async function initializeDatabase() {
126126
process.env.WITH_TIPG === "true" ||
127127
process.env.WITH_TITILER_PGSTAC === "true"
128128
) {
129-
// mmgis-stac
129+
// mmgis-stac
130130
await baseSequelize
131131
.query(`CREATE DATABASE "mmgis-stac";`)
132132
.then(() => {

tests/global-setup.js

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import pgPromise from 'pg-promise';
3030
/** Hardcoded test database name — never changes. */
3131
const TEST_DB_NAME = 'mmgis-test';
3232

33+
/** Hardcoded test STAC database name — used when STAC services are enabled. */
34+
const TEST_STAC_DB_NAME = 'mmgis-stac-test';
35+
3336
/** Port the test server listens on. */
3437
const TEST_PORT = Number(process.env.TEST_PORT || 18888);
3538

@@ -53,11 +56,33 @@ export default async function globalSetup() {
5356
// Load .env so we can read DB_HOST / DB_PORT / DB_USER / DB_PASS
5457
config({ path: resolve(process.cwd(), '.env') });
5558

59+
// ── Production environment fail-safe ──────────────────────────
60+
if (process.env.NODE_ENV === 'production') {
61+
throw new Error(
62+
'\u26A0\uFE0F DANGER: Refusing to run test operations because NODE_ENV is set to "production". ' +
63+
'Tests must never be executed against a production environment.'
64+
);
65+
}
66+
5667
// Read connection settings (with sensible defaults)
5768
const dbHost = process.env.DB_HOST || readDotenvValue('DB_HOST') || 'localhost';
5869
const dbPort = process.env.DB_PORT || readDotenvValue('DB_PORT') || '5432';
59-
const dbUser = process.env.DB_USER || readDotenvValue('DB_USER') || 'mmgis';
60-
const dbPass = process.env.DB_PASS || readDotenvValue('DB_PASS') || 'mmgis';
70+
71+
// Use dedicated test DB credentials (DB_USER_TEST / DB_PASS_TEST) to enforce
72+
// least-privilege separation between CI/test and production database roles.
73+
// No fallback — tests must use explicit test credentials.
74+
const dbUser = process.env.DB_USER_TEST || readDotenvValue('DB_USER_TEST');
75+
const dbPass = process.env.DB_PASS_TEST || readDotenvValue('DB_PASS_TEST');
76+
77+
if (!dbUser || !dbPass) {
78+
throw new Error(
79+
'DB_USER_TEST and DB_PASS_TEST must be set for test setup. ' +
80+
'Set them in your environment or .env file.'
81+
);
82+
}
83+
84+
process.env.DB_USER = dbUser;
85+
process.env.DB_PASS = dbPass;
6186

6287
// Force DB_NAME to the hardcoded test database
6388
process.env.DB_NAME = TEST_DB_NAME;
@@ -168,6 +193,43 @@ export default async function globalSetup() {
168193
await testDb.$pool.end();
169194
}
170195

196+
// ── 3b. Create the STAC test database if STAC services are enabled ──
197+
const stacEnabled =
198+
process.env.WITH_STAC === 'true' ||
199+
process.env.WITH_TIPG === 'true' ||
200+
process.env.WITH_TITILER_PGSTAC === 'true';
201+
202+
if (stacEnabled) {
203+
const stacAdminDb = pgp({
204+
host: dbHost,
205+
port: Number(dbPort),
206+
user: dbUser,
207+
password: dbPass,
208+
database: 'postgres',
209+
});
210+
211+
try {
212+
const stacExists = await stacAdminDb.oneOrNone(
213+
'SELECT 1 FROM pg_database WHERE datname = $1',
214+
[TEST_STAC_DB_NAME],
215+
);
216+
217+
if (!stacExists) {
218+
await stacAdminDb.none('CREATE DATABASE $1:name', [TEST_STAC_DB_NAME]);
219+
console.log(`[global-setup] Created database "${TEST_STAC_DB_NAME}".`);
220+
} else {
221+
console.log(`[global-setup] Database "${TEST_STAC_DB_NAME}" already exists.`);
222+
}
223+
} catch (err) {
224+
console.error(`[global-setup] Failed to create database "${TEST_STAC_DB_NAME}":`, err.message);
225+
throw err;
226+
} finally {
227+
await stacAdminDb.$pool.end();
228+
}
229+
230+
231+
}
232+
171233
// ── 4. Check if Reference Mission already exists ────────────────
172234
let needsMission = true;
173235
const checkDb = pgp({
@@ -234,6 +296,7 @@ export default async function globalSetup() {
234296
cwd: process.cwd(),
235297
stdio: 'pipe',
236298
detached: true,
299+
windowsHide: true,
237300
});
238301

239302
server.stdout.on('data', (d) => {
@@ -398,8 +461,14 @@ function prepareAdjacentServerEnvFiles(repoRoot) {
398461
},
399462
);
400463

464+
// Point adjacent servers at the test STAC database
465+
contents = contents.replace(
466+
/^(POSTGRES_DBNAME\s*=\s*).*$/m,
467+
`$1${TEST_STAC_DB_NAME}`,
468+
);
469+
401470
writeFileSync(envFile, contents, 'utf8');
402-
console.log(`[global-setup] Created ${srv.dir}/.env from .env.example.`);
471+
console.log(`[global-setup] Created ${srv.dir}/.env from .env.example (POSTGRES_DBNAME=${TEST_STAC_DB_NAME}).`);
403472
}
404473
}
405474

@@ -484,16 +553,16 @@ function killProcessOnPort(port) {
484553
if (isWin) {
485554
const out = execSync(
486555
`netstat -ano | findstr :${port} | findstr LISTENING`,
487-
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
556+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true },
488557
).trim();
489558
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
490559
for (const pid of pids) {
491-
try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' }); } catch {}
560+
try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', windowsHide: true }); } catch {}
492561
}
493562
} else {
494563
const out = execSync(
495564
`lsof -ti tcp:${port}`,
496-
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
565+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true },
497566
).trim();
498567
if (out) {
499568
for (const pid of out.split('\n')) {

tests/test-db-clean.js

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/**
2-
* Drop the `mmgis-test` database.
2+
* Drop the `mmgis-test` and `mmgis-stac-test` databases.
33
*
44
* Usage: npm run test:clean
55
*
6-
* Reads DB_HOST / DB_PORT / DB_USER / DB_PASS from the project `.env`
7-
* (or falls back to sensible defaults) and drops the hardcoded
8-
* `mmgis-test` database. Safe to run at any time — only ever touches
9-
* the test database.
6+
* Reads DB_HOST / DB_PORT / DB_USER_TEST / DB_PASS_TEST from the project
7+
* `.env` and drops the hardcoded test databases. Safe to run at any time
8+
* — only ever touches test databases.
109
*/
1110

1211
import { config } from 'dotenv';
@@ -15,6 +14,7 @@ import { readFileSync } from 'fs';
1514
import pgPromise from 'pg-promise';
1615

1716
const TEST_DB_NAME = 'mmgis-test';
17+
const TEST_STAC_DB_NAME = 'mmgis-stac-test';
1818

1919
function readDotenvValue(key) {
2020
try {
@@ -31,10 +31,29 @@ function readDotenvValue(key) {
3131
async function clean() {
3232
config({ path: resolve(process.cwd(), '.env') });
3333

34+
// ── Production environment fail-safe ──────────────────────────
35+
if (process.env.NODE_ENV === 'production') {
36+
throw new Error(
37+
'\u26A0\uFE0F DANGER: Refusing to run test operations because NODE_ENV is set to "production". ' +
38+
'Tests must never be executed against a production environment.'
39+
);
40+
}
41+
3442
const dbHost = process.env.DB_HOST || readDotenvValue('DB_HOST') || 'localhost';
3543
const dbPort = process.env.DB_PORT || readDotenvValue('DB_PORT') || '5432';
36-
const dbUser = process.env.DB_USER || readDotenvValue('DB_USER') || 'mmgis';
37-
const dbPass = process.env.DB_PASS || readDotenvValue('DB_PASS') || 'mmgis';
44+
45+
// Use dedicated test DB credentials (DB_USER_TEST / DB_PASS_TEST) to enforce
46+
// least-privilege separation between CI/test and production database roles.
47+
// No fallback — test-db-clean must use explicit test credentials.
48+
const dbUser = process.env.DB_USER_TEST || readDotenvValue('DB_USER_TEST');
49+
const dbPass = process.env.DB_PASS_TEST || readDotenvValue('DB_PASS_TEST');
50+
51+
if (!dbUser || !dbPass) {
52+
throw new Error(
53+
'DB_USER_TEST and DB_PASS_TEST must be set for test database cleanup. ' +
54+
'Set them in your environment or .env file.'
55+
);
56+
}
3857

3958
const pgp = pgPromise();
4059
const db = pgp({
@@ -46,31 +65,50 @@ async function clean() {
4665
});
4766

4867
try {
68+
// Drop mmgis-test
4969
const exists = await db.oneOrNone(
5070
'SELECT 1 FROM pg_database WHERE datname = $1',
5171
[TEST_DB_NAME],
5272
);
5373

54-
if (!exists) {
55-
console.log(`Database "${TEST_DB_NAME}" does not exist. Nothing to clean.`);
56-
return;
74+
if (exists) {
75+
await db.none(
76+
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
77+
WHERE datname = $1 AND pid <> pg_backend_pid()`,
78+
[TEST_DB_NAME],
79+
);
80+
await db.none('DROP DATABASE $1:name', [TEST_DB_NAME]);
81+
console.log(`Dropped database "${TEST_DB_NAME}".`);
82+
} else {
83+
console.log(`Database "${TEST_DB_NAME}" does not exist — skipping.`);
5784
}
5885

59-
// Terminate active connections before dropping
60-
await db.none(
61-
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
62-
WHERE datname = $1 AND pid <> pg_backend_pid()`,
63-
[TEST_DB_NAME],
86+
// Drop mmgis-stac-test (independent of main test DB)
87+
const stacExists = await db.oneOrNone(
88+
'SELECT 1 FROM pg_database WHERE datname = $1',
89+
[TEST_STAC_DB_NAME],
6490
);
6591

66-
await db.none('DROP DATABASE $1:name', [TEST_DB_NAME]);
67-
console.log(`Dropped database "${TEST_DB_NAME}".`);
92+
if (stacExists) {
93+
await db.none(
94+
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
95+
WHERE datname = $1 AND pid <> pg_backend_pid()`,
96+
[TEST_STAC_DB_NAME],
97+
);
98+
await db.none('DROP DATABASE $1:name', [TEST_STAC_DB_NAME]);
99+
console.log(`Dropped database "${TEST_STAC_DB_NAME}".`);
100+
} else {
101+
console.log(`Database "${TEST_STAC_DB_NAME}" does not exist — skipping.`);
102+
}
68103
} catch (err) {
69-
console.error(`Failed to drop "${TEST_DB_NAME}":`, err.message);
104+
console.error(`Failed to drop test databases:`, err.message);
70105
process.exit(1);
71106
} finally {
72107
pgp.end();
73108
}
74109
}
75110

76-
clean();
111+
clean().catch((err) => {
112+
console.error(err.message);
113+
process.exit(1);
114+
});

0 commit comments

Comments
 (0)