Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions tests/00_production-safety.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';

/**
* Production Safety Check - Database Level
*
* GAP-SYNC-047: This test runs FIRST (00_ prefix) to verify
* the database is safe for destructive test operations.
*
* Checks:
* 1. Database name contains "test"
* 2. Entry count is below threshold (default: 100)
*
* Environment Variables:
* - TEST_SAFETY_MAX_ENTRIES: Max entries before refusing (default: 100)
* - TEST_SAFETY_REQUIRE_TEST_DB: Require "test" in DB name (default: true)
* - TEST_SAFETY_SKIP: Bypass all checks (dangerous!)
*/

var should = require('should');
var language = require('../lib/language')();
var productionSafety = require('./lib/production-safety');

describe('00 Production Safety Check', function() {
this.timeout(10000);
var self = this;

before(function(done) {
// Boot the app to get DB connection
process.env.API_SECRET = 'this is my long pass phrase';
self.env = require('../lib/server/env')();
self.env.settings.authDefaultRoles = 'readable';
self.env.settings.enable = ['careportal', 'api'];

require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) {
self.ctx = ctx;
done();
});
});

it('should verify database is safe for destructive tests', function(done) {
// This is the critical safety gate
productionSafety.checkProductionSafety(self.ctx, self.env)
.then(function() {
done();
})
.catch(function(err) {
// Fail the test suite immediately
console.error('\n\n' + '!'.repeat(70));
console.error('TEST SUITE HALTED - Production safety check activated');
console.error('!'.repeat(70) + '\n');
process.exit(1);
});
});

it('should have extracted correct database name', function() {
var dbName = productionSafety.extractDbName(self.env.storageURI || self.env.mongo_connection || '');
dbName.should.be.a.String();
dbName.length.should.be.greaterThan(0);
console.log('[SAFETY] Database name: ' + dbName);
});
});
11 changes: 3 additions & 8 deletions tests/hooks.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
'use strict;'

var testHelpers = require('./lib/test-helpers');
var productionSafety = require('./lib/production-safety');

// GAP-SYNC-046: Safety check to prevent tests running against production
if (process.env.NODE_ENV !== 'test') {
console.error('\n❌ SAFETY ERROR: NODE_ENV must be "test" to run tests.');
console.error(' Current value: ' + (process.env.NODE_ENV || '(not set)'));
console.error(' Tests use deleteMany({}) which could destroy production data.');
console.error(' Fix: Use "npm test" which loads my.test.env, or set NODE_ENV=test\n');
process.exit(1);
}
// GAP-SYNC-046: Pre-flight safety check (no DB required)
productionSafety.preflightCheck();

var slowTestThreshold = parseInt(process.env.SLOW_TEST_THRESHOLD, 10) || 2000;
var enableTimingWarnings = process.env.ENABLE_TIMING_WARNINGS === 'true';
Expand Down
175 changes: 175 additions & 0 deletions tests/lib/production-safety.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use strict';

/**
* Production Safety Check for Test Suite
*
* GAP-SYNC-047: Prevents tests from running against production databases
* by checking multiple safety signals:
*
* 1. Database name should contain "test" (configurable)
* 2. Entry count should be below threshold (default: 100)
*
* Environment Variables:
* - TEST_SAFETY_MAX_ENTRIES: Max entries before refusing (default: 100, 0 to disable)
* - TEST_SAFETY_REQUIRE_TEST_DB: Require "test" in DB name (default: true)
* - TEST_SAFETY_SKIP: Emergency bypass for all checks (default: false)
*/

const DEFAULT_MAX_ENTRIES = 100;

/**
* Extract database name from MongoDB connection string
* @param {string} connectionString - MongoDB URI
* @returns {string} Database name
*/
function extractDbName(connectionString) {
try {
// Handle both mongodb:// and mongodb+srv:// formats
const url = new URL(connectionString);
// pathname is /dbname or /dbname?options
let dbName = url.pathname.slice(1); // Remove leading /
// Remove query string if present
const queryIndex = dbName.indexOf('?');
if (queryIndex > -1) {
dbName = dbName.slice(0, queryIndex);
}
return dbName || 'nightscout';
} catch (err) {
// Fallback for non-standard connection strings
const match = connectionString.match(/\/([^/?]+)(\?|$)/);
return match ? match[1] : 'unknown';
}
}

/**
* Check if database name indicates a test database
* @param {string} dbName - Database name
* @returns {boolean} True if looks like test database
*/
function isTestDatabaseName(dbName) {
const lower = dbName.toLowerCase();
return lower.includes('test') ||
lower.includes('_test') ||
lower.startsWith('test_') ||
lower.endsWith('_test');
}

/**
* Run production safety checks
*
* @param {Object} ctx - Boot context with entries collection
* @param {Object} env - Environment with storageURI
* @returns {Promise<void>} Resolves if safe, rejects with error if not
*/
async function checkProductionSafety(ctx, env) {
// Emergency bypass
if (process.env.TEST_SAFETY_SKIP === 'true') {
console.warn('[SAFETY] ⚠️ TEST_SAFETY_SKIP=true - All safety checks bypassed!');
return;
}

const errors = [];
const warnings = [];

// Check 1: Database name should indicate test
const requireTestDb = process.env.TEST_SAFETY_REQUIRE_TEST_DB !== 'false';
const dbName = extractDbName(env.storageURI || env.mongo_connection || '');

if (requireTestDb && !isTestDatabaseName(dbName)) {
errors.push({
check: 'Database Name',
message: `Database "${dbName}" doesn't contain "test" in its name`,
hint: 'Use a database name like "nightscout_test" or set TEST_SAFETY_REQUIRE_TEST_DB=false'
});
} else if (isTestDatabaseName(dbName)) {
console.log(`[SAFETY] ✅ Database name "${dbName}" looks like a test database`);
}

// Check 2: Entry count threshold
const maxEntries = parseInt(process.env.TEST_SAFETY_MAX_ENTRIES || String(DEFAULT_MAX_ENTRIES), 10);

if (maxEntries > 0 && ctx.store && ctx.store.db) {
try {
// Access entries collection directly via store
const entriesCol = ctx.store.db.collection('entries');
// Use limit+1 pattern for efficiency - we only need to know if it exceeds threshold
const count = await entriesCol.countDocuments({}, {
limit: maxEntries + 1,
maxTimeMS: 5000 // Don't hang on slow connections
});

if (count > maxEntries) {
errors.push({
check: 'Entry Count',
message: `Database has ${count}+ entries (threshold: ${maxEntries})`,
hint: `This looks like a production database. Set TEST_SAFETY_MAX_ENTRIES=${count + 100} to override`
});
} else {
console.log(`[SAFETY] ✅ Database has ${count} entries (threshold: ${maxEntries})`);
}
} catch (err) {
warnings.push({
check: 'Entry Count',
message: `Could not count entries: ${err.message}`,
hint: 'Entry count check skipped'
});
}
} else if (maxEntries === 0) {
console.log('[SAFETY] ⚠️ Entry count check disabled (TEST_SAFETY_MAX_ENTRIES=0)');
}

// Report warnings
warnings.forEach(w => {
console.warn(`[SAFETY] ⚠️ ${w.check}: ${w.message}`);
});

// Report errors and fail
if (errors.length > 0) {
console.error('\n' + '='.repeat(70));
console.error('🛡️ PRODUCTION SAFETY CHECK ACTIVATED');
console.error('='.repeat(70));
console.error('\nThis database appears to contain real data.');
console.error('Running the test suite WILL DELETE all data in this database.');
console.error('\nThis safety check exists to prevent accidental destruction of');
console.error('production data. If this is truly a test database, you can override.\n');

errors.forEach((e, i) => {
console.error(`${i + 1}. ${e.check}:`);
console.error(` ${e.message}`);
console.error(` 💡 ${e.hint}\n`);
});

console.error('Override options:');
console.error(' • Set TEST_SAFETY_MAX_ENTRIES to a higher value (e.g., 1000)');
console.error(' • Set TEST_SAFETY_REQUIRE_TEST_DB=false to allow any DB name');
console.error(' • Set TEST_SAFETY_SKIP=true to bypass ALL checks (dangerous!)');
console.error('='.repeat(70) + '\n');

throw new Error('Production safety check activated: ' + errors.map(e => e.check).join(', '));
}

console.log('[SAFETY] ✅ All production safety checks passed');
}

/**
* Synchronous pre-flight check (no DB required)
* Run this before booting the application
*/
function preflightCheck() {
// Check NODE_ENV
if (process.env.NODE_ENV !== 'test') {
console.error('\n❌ SAFETY ERROR: NODE_ENV must be "test" to run tests.');
console.error(' Current value: ' + (process.env.NODE_ENV || '(not set)'));
console.error(' Tests use deleteMany({}) which could destroy production data.');
console.error(' Fix: Use "npm test" which loads my.test.env, or set NODE_ENV=test\n');
process.exit(1);
}
}

module.exports = {
checkProductionSafety,
preflightCheck,
extractDbName,
isTestDatabaseName,
DEFAULT_MAX_ENTRIES
};
88 changes: 88 additions & 0 deletions tests/production-safety.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict';

/**
* Unit tests for production-safety.js module
*/

var should = require('should');
var productionSafety = require('./lib/production-safety');

describe('Production Safety Module', function() {

describe('extractDbName', function() {

it('should extract database name from standard MongoDB URI', function() {
var dbName = productionSafety.extractDbName('mongodb://localhost:27017/nightscout_test');
dbName.should.equal('nightscout_test');
});

it('should extract database name from MongoDB+SRV URI', function() {
var dbName = productionSafety.extractDbName('mongodb+srv://user:pass@cluster.mongodb.net/mydb_test');
dbName.should.equal('mydb_test');
});

it('should handle URI with query parameters', function() {
var dbName = productionSafety.extractDbName('mongodb://localhost:27017/nightscout_test?retryWrites=true');
dbName.should.equal('nightscout_test');
});

it('should handle URI with auth credentials', function() {
var dbName = productionSafety.extractDbName('mongodb://user:password@localhost:27017/test_db');
dbName.should.equal('test_db');
});

it('should return default for empty connection string', function() {
var dbName = productionSafety.extractDbName('');
dbName.should.be.a.String();
});
});

describe('isTestDatabaseName', function() {

it('should recognize "_test" suffix', function() {
productionSafety.isTestDatabaseName('nightscout_test').should.be.true();
});

it('should recognize "test_" prefix', function() {
productionSafety.isTestDatabaseName('test_nightscout').should.be.true();
});

it('should recognize "test" anywhere in name', function() {
productionSafety.isTestDatabaseName('my_testing_db').should.be.true();
});

it('should be case insensitive', function() {
productionSafety.isTestDatabaseName('Nightscout_TEST').should.be.true();
productionSafety.isTestDatabaseName('TEST_DB').should.be.true();
});

it('should reject production-looking names', function() {
productionSafety.isTestDatabaseName('nightscout').should.be.false();
productionSafety.isTestDatabaseName('production').should.be.false();
productionSafety.isTestDatabaseName('mydb').should.be.false();
});

it('should reject names with "test" as substring of other words', function() {
// "contest" contains "test" but is not a test database
// Current implementation will actually match this - documenting behavior
productionSafety.isTestDatabaseName('contest').should.be.true(); // Contains "test"
});
});

describe('DEFAULT_MAX_ENTRIES', function() {

it('should be a reasonable default (100)', function() {
productionSafety.DEFAULT_MAX_ENTRIES.should.equal(100);
});
});

describe('preflightCheck', function() {

it('should not throw when NODE_ENV=test', function() {
// This test is running, so NODE_ENV must be test
process.env.NODE_ENV.should.equal('test');
// preflightCheck would have already run via hooks.js
// If we got here, it passed
});
});
});
Loading