fix: production safety allows testing when entries within threshold (#8464)

The DB name check now only blocks when entries EXCEED the threshold.
If the database has fewer entries than the threshold (default 100),
it's treated as safe to test regardless of DB name — the name check
becomes a warning suggesting you rename for best practice.

Logic: entry count is the primary safety signal. DB name is secondary.
Both must fail to block. Entry count alone blocks. Name alone warns.

Changes:
- Entry count checked first to determine safety baseline
- DB name check downgrades to warning when entries within threshold
- Contextual override hints (only show relevant suggestions)
- Clarify CUSTOMCONNSTR_mongo vs CUSTOMCONNSTR_mongo_collection

Tests (5 new):
- 0 entries + non-test name → passes with warning
- 50 entries (below threshold) + non-test name → passes with warning
- Entries above threshold + non-test name → blocks (both errors)
- Entries above threshold alone → blocks with 'real data' message
- Non-test name hint mentions correct env var

Fixes nightscout/cgm-remote-monitor#8464

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben West
2026-04-19 12:13:48 -07:00
co-authored by Copilot
parent 15a5fb24d3
commit 3b701222e2
2 changed files with 275 additions and 27 deletions
+52 -27
View File
@@ -71,41 +71,28 @@ async function checkProductionSafety(ctx, env) {
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
// Check 1: Entry count threshold (run first — an empty DB is always safe)
const maxEntries = parseInt(process.env.TEST_SAFETY_MAX_ENTRIES || String(DEFAULT_MAX_ENTRIES), 10);
let entryCount = 0;
let entryCountChecked = false;
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({}, {
entryCount = await entriesCol.countDocuments({}, {
limit: maxEntries + 1,
maxTimeMS: 5000 // Don't hang on slow connections
maxTimeMS: 5000
});
entryCountChecked = true;
if (count > maxEntries) {
if (entryCount > 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`
message: `Database has ${entryCount}+ entries (threshold: ${maxEntries})`,
hint: `This looks like a production database. Set TEST_SAFETY_MAX_ENTRIES=${entryCount + 100} to override`
});
} else {
console.log(`[SAFETY] ✅ Database has ${count} entries (threshold: ${maxEntries})`);
console.log(`[SAFETY] ✅ Database has ${entryCount} entries (threshold: ${maxEntries})`);
}
} catch (err) {
warnings.push({
@@ -118,6 +105,31 @@ async function checkProductionSafety(ctx, env) {
console.log('[SAFETY] ⚠️ Entry count check disabled (TEST_SAFETY_MAX_ENTRIES=0)');
}
// Check 2: Database name should indicate test
// If entry count is within threshold, the DB is safe regardless of name — downgrade to warning
const requireTestDb = process.env.TEST_SAFETY_REQUIRE_TEST_DB !== 'false';
const dbName = extractDbName(env.storageURI || env.mongo_connection || '');
const entryCountSafe = entryCountChecked && entryCount <= maxEntries;
if (requireTestDb && !isTestDatabaseName(dbName)) {
if (entryCountSafe) {
console.log(`[SAFETY] ✅ Database "${dbName}" has ${entryCount} entries (within threshold) — safe to test`);
warnings.push({
check: 'Database Name',
message: `Database "${dbName}" doesn't contain "test" in its name (allowed because entry count is within threshold)`,
hint: 'Consider renaming: set CUSTOMCONNSTR_mongo=mongodb://localhost/nightscout_test in my.test.env'
});
} else {
errors.push({
check: 'Database Name',
message: `Database "${dbName}" doesn't contain "test" in its name`,
hint: 'Set the database name in CUSTOMCONNSTR_mongo (e.g., mongodb://localhost/nightscout_test)\n Note: CUSTOMCONNSTR_mongo_collection sets the entries collection, not the DB name.\n Or set TEST_SAFETY_REQUIRE_TEST_DB=false to allow any DB name'
});
}
} else if (isTestDatabaseName(dbName)) {
console.log(`[SAFETY] ✅ Database name "${dbName}" looks like a test database`);
}
// Report warnings
warnings.forEach(w => {
console.warn(`[SAFETY] ⚠️ ${w.check}: ${w.message}`);
@@ -125,11 +137,19 @@ async function checkProductionSafety(ctx, env) {
// Report errors and fail
if (errors.length > 0) {
const hasEntryCountError = errors.some(e => e.check === 'Entry Count');
const hasDbNameError = errors.some(e => e.check === 'Database Name');
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.');
if (hasEntryCountError) {
console.error('\nThis database appears to contain real data.');
console.error('Running the test suite WILL DELETE all data in this database.');
} else {
console.error('\nThis database does not look like a test database.');
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');
@@ -140,8 +160,13 @@ async function checkProductionSafety(ctx, env) {
});
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');
if (hasDbNameError) {
console.error(' • Rename your test DB: set CUSTOMCONNSTR_mongo=mongodb://localhost/nightscout_test in my.test.env');
console.error(' • Or set TEST_SAFETY_REQUIRE_TEST_DB=false to allow any DB name');
}
if (hasEntryCountError) {
console.error(' • Set TEST_SAFETY_MAX_ENTRIES to a higher value if entries are expected');
}
console.error(' • Set TEST_SAFETY_SKIP=true to bypass ALL checks (dangerous!)');
console.error('='.repeat(70) + '\n');
+223
View File
@@ -85,4 +85,227 @@ describe('Production Safety Module', function() {
// If we got here, it passed
});
});
describe('checkProductionSafety messaging', function() {
var savedEnv;
beforeEach(function() {
savedEnv = {
TEST_SAFETY_SKIP: process.env.TEST_SAFETY_SKIP,
TEST_SAFETY_REQUIRE_TEST_DB: process.env.TEST_SAFETY_REQUIRE_TEST_DB,
TEST_SAFETY_MAX_ENTRIES: process.env.TEST_SAFETY_MAX_ENTRIES
};
});
afterEach(function() {
// Restore env
Object.keys(savedEnv).forEach(function(k) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
});
});
it('should pass when database is empty even if name lacks "test"', function(done) {
process.env.TEST_SAFETY_REQUIRE_TEST_DB = 'true';
delete process.env.TEST_SAFETY_MAX_ENTRIES;
delete process.env.TEST_SAFETY_SKIP;
var mockCtx = {
store: {
db: {
collection: function() {
return {
countDocuments: function(query, opts) {
return Promise.resolve(0);
}
};
}
}
}
};
var mockEnv = { storageURI: 'mongodb://localhost:27017/nightscout' };
var output = [];
var originalLog = console.log;
var originalWarn = console.warn;
console.log = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
console.warn = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
productionSafety.checkProductionSafety(mockCtx, mockEnv)
.then(function() {
console.log = originalLog;
console.warn = originalWarn;
var fullOutput = output.join('\n');
fullOutput.should.match(/within threshold.*safe to test/);
fullOutput.should.match(/doesn't contain "test"/);
done();
})
.catch(function(err) {
console.log = originalLog;
console.warn = originalWarn;
done(err);
});
});
it('should pass when entries are below threshold even if name lacks "test"', function(done) {
process.env.TEST_SAFETY_REQUIRE_TEST_DB = 'true';
delete process.env.TEST_SAFETY_MAX_ENTRIES; // default 100
delete process.env.TEST_SAFETY_SKIP;
var mockCtx = {
store: {
db: {
collection: function() {
return {
countDocuments: function() {
return Promise.resolve(50);
}
};
}
}
}
};
var mockEnv = { storageURI: 'mongodb://localhost:27017/nightscout' };
var output = [];
var originalLog = console.log;
var originalWarn = console.warn;
console.log = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
console.warn = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
productionSafety.checkProductionSafety(mockCtx, mockEnv)
.then(function() {
console.log = originalLog;
console.warn = originalWarn;
var fullOutput = output.join('\n');
fullOutput.should.match(/50 entries.*within threshold.*safe to test/);
done();
})
.catch(function(err) {
console.log = originalLog;
console.warn = originalWarn;
done(err);
});
});
it('should say "contains real data" when entry count exceeds threshold', function(done) {
process.env.TEST_SAFETY_REQUIRE_TEST_DB = 'false';
process.env.TEST_SAFETY_MAX_ENTRIES = '10';
delete process.env.TEST_SAFETY_SKIP;
var mockCtx = {
store: {
db: {
collection: function() {
return {
countDocuments: function() {
return Promise.resolve(500);
}
};
}
}
}
};
var mockEnv = { storageURI: 'mongodb://localhost:27017/nightscout' };
var output = [];
var originalError = console.error;
console.error = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
productionSafety.checkProductionSafety(mockCtx, mockEnv)
.then(function() {
console.error = originalError;
should.fail('should have thrown');
})
.catch(function(err) {
console.error = originalError;
err.message.should.match(/Entry Count/);
var fullOutput = output.join('\n');
fullOutput.should.match(/appears to contain real data/);
fullOutput.should.match(/TEST_SAFETY_MAX_ENTRIES/);
done();
});
});
it('hint should mention CUSTOMCONNSTR_mongo not mongo_collection', function(done) {
process.env.TEST_SAFETY_REQUIRE_TEST_DB = 'true';
process.env.TEST_SAFETY_MAX_ENTRIES = '0'; // disable entry check
delete process.env.TEST_SAFETY_SKIP;
var mockCtx = { store: { db: null } };
var mockEnv = { storageURI: 'mongodb://localhost:27017/production_db' };
var output = [];
var originalError = console.error;
console.error = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
productionSafety.checkProductionSafety(mockCtx, mockEnv)
.then(function() {
console.error = originalError;
should.fail('should have thrown');
})
.catch(function(err) {
console.error = originalError;
var fullOutput = output.join('\n');
// Should guide user to the correct env var
fullOutput.should.match(/CUSTOMCONNSTR_mongo/);
fullOutput.should.match(/mongo_collection sets the entries collection, not the DB name/);
done();
});
});
it('should block non-test DB name when entries exceed threshold', function(done) {
process.env.TEST_SAFETY_REQUIRE_TEST_DB = 'true';
process.env.TEST_SAFETY_MAX_ENTRIES = '10';
delete process.env.TEST_SAFETY_SKIP;
var mockCtx = {
store: {
db: {
collection: function() {
return {
countDocuments: function() {
return Promise.resolve(50);
}
};
}
}
}
};
var mockEnv = { storageURI: 'mongodb://localhost:27017/nightscout' };
var output = [];
var originalError = console.error;
console.error = function() {
output.push(Array.prototype.join.call(arguments, ' '));
};
productionSafety.checkProductionSafety(mockCtx, mockEnv)
.then(function() {
console.error = originalError;
should.fail('should have thrown');
})
.catch(function(err) {
console.error = originalError;
// Both checks should fail
err.message.should.match(/Entry Count/);
err.message.should.match(/Database Name/);
var fullOutput = output.join('\n');
fullOutput.should.match(/appears to contain real data/);
done();
});
});
});
});