Harden MongoDB driver compatibility

This commit is contained in:
Andy Low
2026-04-19 23:24:22 +01:00
parent e807e1a6b6
commit 69b620dd2f
45 changed files with 2058 additions and 1051 deletions
+4 -8
View File
@@ -26,16 +26,12 @@ describe('Clean MONGO after tests', function ( ) {
});
});
it('wipe treatment data', function (done) {
self.ctx.treatments().deleteMany({ }, function ( ) {
done();
});
it('wipe treatment data', async function () {
await self.ctx.treatments().deleteMany({ });
});
it('wipe entries data', function (done) {
self.ctx.entries().deleteMany({ }, function ( ) {
done();
});
it('wipe entries data', async function () {
await self.ctx.entries().deleteMany({ });
});
});
+8 -6
View File
@@ -55,12 +55,14 @@ describe('v1 API Deduplication Behavior', function() {
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
}, function() {
// Use deleteMany for faster cleanup of entries
self.ctx.entries().deleteMany({}, function() {
// Also clear devicestatus to reduce database load
self.ctx.devicestatus.remove({
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
}, done);
});
self.ctx.entries().deleteMany({})
.then(function() {
// Also clear devicestatus to reduce database load
self.ctx.devicestatus.remove({
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
}, done);
})
.catch(done);
});
});
+4 -4
View File
@@ -62,12 +62,12 @@ describe('Entries REST api', function ( ) {
});
afterEach(function (done) {
self.archive( ).deleteMany({ }, done);
afterEach(async function () {
await self.archive( ).deleteMany({ });
});
after(function (done) {
self.archive( ).deleteMany({ }, done);
after(async function () {
await self.archive( ).deleteMany({ });
});
// keep this test pinned at or near the top in order to validate all
+80 -82
View File
@@ -43,12 +43,12 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
});
});
afterEach(function(done) {
self.archive().deleteMany({}, done);
afterEach(async function() {
await self.archive().deleteMany({});
});
after(function(done) {
self.archive().deleteMany({}, done);
after(async function() {
await self.archive().deleteMany({});
});
/**
@@ -98,14 +98,14 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
if (err2) return done(err2);
// Verify: only 1 entry exists with updated sgv 125
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
if (err3) return done(err3);
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
docs[0].direction.should.equal('FortyFiveUp');
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
docs[0].direction.should.equal('FortyFiveUp');
done();
})
.catch(done);
});
});
});
@@ -155,14 +155,14 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
if (err2) return done(err2);
// Verify: 2 entries exist (different types)
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
if (err3) return done(err3);
docs.should.have.lengthOf(2);
var types = docs.map(d => d.type).sort();
types.should.eql(['mbg', 'sgv']);
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(2);
var types = docs.map(d => d.type).sort();
types.should.eql(['mbg', 'sgv']);
done();
})
.catch(done);
});
});
});
@@ -213,12 +213,12 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
if (err2) return done(err2);
// Verify: 2 entries exist
self.archive().find({ type: 'sgv', date: { $in: [timestamp1, timestamp2] } }).toArray(function(err3, docs) {
if (err3) return done(err3);
docs.should.have.lengthOf(2);
done();
});
self.archive().find({ type: 'sgv', date: { $in: [timestamp1, timestamp2] } }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(2);
done();
})
.catch(done);
});
});
});
@@ -249,12 +249,12 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
});
});
afterEach(function(done) {
self.archive().deleteMany({}, done);
afterEach(async function() {
await self.archive().deleteMany({});
});
after(function(done) {
self.archive().deleteMany({}, done);
after(async function() {
await self.archive().deleteMany({});
});
/**
@@ -286,14 +286,14 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
if (err) return done(err);
// Entry should be created (may have ObjectId _id, UUID in identifier)
self.archive().find({ date: timestamp }).toArray(function(err2, docs) {
if (err2) return done(err2);
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(120);
// Note: After fix, expect docs[0].identifier === uuid
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(120);
// Note: After fix, expect docs[0].identifier === uuid
done();
})
.catch(done);
});
});
@@ -347,13 +347,13 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
if (err2) return done(err2);
// Verify: single entry, updated value
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
if (err3) return done(err3);
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
done();
})
.catch(done);
});
});
});
@@ -412,13 +412,13 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
if (err2) return done(err2);
// Verify: single entry (dedup by sysTime+type, not UUID)
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
if (err3) return done(err3);
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
done();
})
.catch(done);
});
});
});
@@ -469,14 +469,14 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
if (err) return done(err);
// Verify: all 3 entries created
self.archive().find({ date: { $in: [timestamp1, timestamp2, timestamp3] } }).toArray(function(err2, docs) {
if (err2) return done(err2);
docs.should.have.lengthOf(3);
var sgvValues = docs.map(d => d.sgv).sort();
sgvValues.should.eql([120, 125, 130]);
done();
});
self.archive().find({ date: { $in: [timestamp1, timestamp2, timestamp3] } }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(3);
var sgvValues = docs.map(d => d.sgv).sort();
sgvValues.should.eql([120, 125, 130]);
done();
})
.catch(done);
});
});
@@ -500,9 +500,7 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
dateString: sysTime,
sysTime: sysTime,
device: 'Trio'
}, function(err) {
if (err) return done(err);
}).then(function() {
// POST via API with same timestamp
var entry = {
_id: uuid,
@@ -513,7 +511,7 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
dateString: sysTime,
device: 'Trio'
};
request(self.app)
.post('/entries/')
.set('api-secret', self.known)
@@ -521,17 +519,17 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
.expect(200)
.end(function(err2, res) {
if (err2) return done(err2);
// Verify: single entry, updated value
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
if (err3) return done(err3);
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(1);
docs[0].sgv.should.equal(125);
done();
})
.catch(done);
});
});
}).catch(done);
});
/**
@@ -563,15 +561,15 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
if (err) return done(err);
// Verify: entry has identifier field with UUID
self.archive().find({ date: timestamp }).toArray(function(err2, docs) {
if (err2) return done(err2);
docs.should.have.lengthOf(1);
docs[0].should.have.property('identifier', uuid);
// _id should be ObjectId, not UUID
docs[0]._id.should.not.equal(uuid);
done();
});
self.archive().find({ date: timestamp }).toArray()
.then(function(docs) {
docs.should.have.lengthOf(1);
docs[0].should.have.property('identifier', uuid);
// _id should be ObjectId, not UUID
docs[0]._id.should.not.equal(uuid);
done();
})
.catch(done);
});
});
});
+34
View File
@@ -0,0 +1,34 @@
'use strict';
var should = require('should');
var objectIdValidation = require('../lib/api/shared/objectid-validation');
describe('API ObjectId validation helper', function () {
it('accepts missing ids for create-tolerant routes', function () {
should(objectIdValidation.isValidObjectId(undefined)).be.true();
should(objectIdValidation.isValidObjectId(null)).be.true();
});
it('accepts 24-character hex ids and rejects non-hex values', function () {
should(objectIdValidation.isValidObjectId('507f1f77bcf86cd799439011')).be.true();
should(objectIdValidation.isValidObjectId('550e8400-e29b-41d4-a716-446655440000')).be.false();
should(objectIdValidation.isValidObjectId(123)).be.false();
});
it('finds the first invalid id in a batch', function () {
objectIdValidation.findInvalidId([
{ _id: '507f1f77bcf86cd799439011' },
{ _id: 'invalid-id' },
{ _id: '507f191e810c19729de860ea' }
]).should.eql({ index: 1, id: 'invalid-id' });
});
it('returns null when all ids are valid or omitted', function () {
should.not.exist(objectIdValidation.findInvalidId([
{ _id: '507f1f77bcf86cd799439011' },
{},
{ _id: null }
]));
});
});
+153
View File
@@ -15,6 +15,22 @@ describe('Security of REST API V1', function() {
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
function rolesCollection() {
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'roles');
}
function subjectsCollection() {
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'subjects');
}
async function getBearerToken(accessToken) {
const res = await request(self.app)
.get('/api/v2/authorization/request/' + accessToken)
.expect(200);
return res.body.token;
}
before(function(done) {
var api = require('../lib/api/');
delete process.env.API_SECRET;
@@ -26,6 +42,7 @@ describe('Security of REST API V1', function() {
self.app = require('express')();
self.app.enable('api');
require('../lib/server/bootevent')(self.env, language).boot(async function booted (ctx) {
self.ctx = ctx;
self.app.use('/api/v1', api(self.env, ctx));
self.app.use('/api/v2/authorization', ctx.authorization.endpoints);
let authResult = await authSubject(ctx.authorization.storage);
@@ -159,4 +176,140 @@ describe('Security of REST API V1', function() {
});
});
describe('Authorization admin save endpoints', function () {
beforeEach(async function () {
await rolesCollection().deleteMany({ name: /^api-security-role/ });
await subjectsCollection().deleteMany({ name: /^api-security-subject/ });
});
afterEach(async function () {
await rolesCollection().deleteMany({ name: /^api-security-role/ });
await subjectsCollection().deleteMany({ name: /^api-security-subject/ });
});
it('PUT /api/v2/authorization/subjects updates an existing subject by _id', async function () {
const insertResult = await subjectsCollection().insertOne({
name: 'api-security-subject-update',
roles: ['readable'],
notes: 'original',
created_at: '2024-10-26T20:32:49.173Z'
});
const token = await getBearerToken(self.token.adminAll);
await request(self.app)
.put('/api/v2/authorization/subjects')
.set('Authorization', 'Bearer ' + token)
.send({
_id: insertResult.insertedId.toString(),
name: 'api-security-subject-update',
roles: ['admin'],
notes: 'updated',
created_at: '2024-10-26T21:32:49.173Z'
})
.expect(200);
const docs = await subjectsCollection().find({ name: 'api-security-subject-update' }).toArray();
docs.length.should.equal(1);
docs[0].roles.should.deepEqual(['admin']);
docs[0].notes.should.equal('updated');
});
it('PUT /api/v2/authorization/roles updates an existing role by _id', async function () {
const insertResult = await rolesCollection().insertOne({
name: 'api-security-role-update',
permissions: ['api:entries:read'],
notes: 'original',
created_at: '2024-10-26T20:32:49.173Z'
});
const token = await getBearerToken(self.token.adminAll);
await request(self.app)
.put('/api/v2/authorization/roles')
.set('Authorization', 'Bearer ' + token)
.send({
_id: insertResult.insertedId.toString(),
name: 'api-security-role-update',
permissions: ['api:entries:update'],
notes: 'updated',
created_at: '2024-10-26T21:32:49.173Z'
})
.expect(200);
const docs = await rolesCollection().find({ name: 'api-security-role-update' }).toArray();
docs.length.should.equal(1);
docs[0].permissions.should.deepEqual(['api:entries:update']);
docs[0].notes.should.equal('updated');
});
it('PUT /api/v2/authorization/subjects fails when _id is missing', async function () {
const token = await getBearerToken(self.token.adminAll);
await request(self.app)
.put('/api/v2/authorization/subjects')
.set('Authorization', 'Bearer ' + token)
.send({
name: 'api-security-subject-missing-id',
roles: ['readable'],
notes: 'should fail'
})
.expect(500);
const docs = await subjectsCollection().find({ name: 'api-security-subject-missing-id' }).toArray();
docs.length.should.equal(0);
});
it('PUT /api/v2/authorization/subjects fails when _id is invalid', async function () {
const token = await getBearerToken(self.token.adminAll);
await request(self.app)
.put('/api/v2/authorization/subjects')
.set('Authorization', 'Bearer ' + token)
.send({
_id: 'not-a-valid-objectid',
name: 'api-security-subject-invalid-id',
roles: ['readable'],
notes: 'should fail'
})
.expect(500);
const docs = await subjectsCollection().find({ name: 'api-security-subject-invalid-id' }).toArray();
docs.length.should.equal(0);
});
it('PUT /api/v2/authorization/roles fails when _id is missing', async function () {
const token = await getBearerToken(self.token.adminAll);
await request(self.app)
.put('/api/v2/authorization/roles')
.set('Authorization', 'Bearer ' + token)
.send({
name: 'api-security-role-missing-id',
permissions: ['api:entries:read'],
notes: 'should fail'
})
.expect(500);
const docs = await rolesCollection().find({ name: 'api-security-role-missing-id' }).toArray();
docs.length.should.equal(0);
});
it('PUT /api/v2/authorization/roles fails when _id is invalid', async function () {
const token = await getBearerToken(self.token.adminAll);
await request(self.app)
.put('/api/v2/authorization/roles')
.set('Authorization', 'Bearer ' + token)
.send({
_id: 'not-a-valid-objectid',
name: 'api-security-role-invalid-id',
permissions: ['api:entries:read'],
notes: 'should fail'
})
.expect(500);
const docs = await rolesCollection().find({ name: 'api-security-role-invalid-id' }).toArray();
docs.length.should.equal(0);
});
});
});
+93 -9
View File
@@ -352,16 +352,12 @@ describe('API Shape Handling - Single Object vs Array Input', function () {
describe('Entries API - /api/entries/', function () {
beforeEach(function (done) {
self.ctx.entries().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await self.ctx.entries().deleteMany({});
});
afterEach(function (done) {
self.ctx.entries().deleteMany({}, function () {
done();
});
afterEach(async function () {
await self.ctx.entries().deleteMany({});
});
it('POST accepts single SGV entry object', function (done) {
@@ -838,7 +834,95 @@ describe('API Shape Handling - Single Object vs Array Input', function () {
p.should.have.property('_id');
});
done();
});
});
});
});
describe('Legacy PUT update semantics', function () {
beforeEach(async function () {
await self.ctx.food().deleteMany({});
await self.ctx.activity().deleteMany({});
});
afterEach(async function () {
await self.ctx.food().deleteMany({});
await self.ctx.activity().deleteMany({});
});
it('PUT /api/food/ updates an existing record by _id when created_at changes', function (done) {
self.ctx.food.create({
name: 'API shape food',
category: 'Test',
carbs: 20,
protein: 10,
fat: 5
}, function (createErr, docs) {
if (createErr) return done(createErr);
var created = docs[0];
request(self.app)
.put('/api/food/')
.set('api-secret', known)
.send({
_id: created._id.toString(),
name: 'API shape food',
category: 'Test',
carbs: 25,
protein: 10,
fat: 5,
created_at: '2024-10-26T21:32:49.173Z'
})
.expect(200)
.end(function (err) {
if (err) return done(err);
self.ctx.food().find({ _id: created._id }).toArray()
.then(function (updatedDocs) {
updatedDocs.length.should.equal(1);
updatedDocs[0].carbs.should.equal(25);
updatedDocs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
})
.catch(done);
});
});
});
it('PUT /api/activity/ updates an existing record by _id', function (done) {
self.ctx.activity.create([{
created_at: '2024-10-26T20:32:49.173Z',
heartrate: 80,
steps: 100,
activitylevel: 'walking'
}], function (createErr, docs) {
if (createErr) return done(createErr);
var created = docs[0];
request(self.app)
.put('/api/activity/')
.set('api-secret', known)
.send({
_id: created._id.toString(),
created_at: '2024-10-26T21:32:49.173Z',
heartrate: 95,
steps: 250,
activitylevel: 'running'
})
.expect(200)
.end(function (err) {
if (err) return done(err);
self.ctx.activity().find({ _id: created._id }).toArray()
.then(function (updatedDocs) {
updatedDocs.length.should.equal(1);
updatedDocs[0].heartrate.should.equal(95);
updatedDocs[0].steps.should.equal(250);
updatedDocs[0].activitylevel.should.equal('running');
done();
})
.catch(done);
});
});
});
});
});
+4 -4
View File
@@ -38,12 +38,12 @@ describe('authed REST api', function ( ) {
this.archive.create(creating, done);
});
afterEach(function (done) {
this.archive( ).deleteMany({ }, done);
afterEach(async function () {
await this.archive( ).deleteMany({ });
});
after(function (done) {
this.archive( ).deleteMany({ }, done);
after(async function () {
await this.archive( ).deleteMany({ });
});
it('disallow unauthorized POST', function (done) {
+44 -7
View File
@@ -3,13 +3,17 @@
require('should');
const find = require('../lib/api3/storage/mongoCollection/find');
const { ObjectId } = require('mongodb');
describe('API3 mongoCollection findMany', function () {
function createStubCollection (observed) {
describe('API3 mongoCollection find helpers', function () {
function createStubCursor (observed, docs) {
return {
find: function () {
return this;
},
project: function () {
return this;
},
sort: function () {
return this;
},
@@ -21,11 +25,17 @@ describe('API3 mongoCollection findMany', function () {
observed.skip = value;
return this;
},
project: function () {
return this;
},
toArray: function (callback) {
callback(null, []);
toArray: function () {
observed.toArrayCalls = (observed.toArrayCalls || 0) + 1;
return Promise.resolve(docs || []);
}
};
}
function createStubCollection (observed, docs) {
return {
find: function () {
return createStubCursor(observed, docs);
}
};
}
@@ -44,6 +54,7 @@ describe('API3 mongoCollection findMany', function () {
observed.limit.should.equal(5);
observed.skip.should.equal(2);
observed.toArrayCalls.should.equal(1);
result.should.eql([]);
});
@@ -63,4 +74,30 @@ describe('API3 mongoCollection findMany', function () {
observed.limit.should.equal(5);
observed.skip.should.equal(2);
});
it('normalizes findOne results when using promise-based cursors', async function () {
const observed = {};
const docId = new ObjectId();
const col = createStubCollection(observed, [{ _id: docId, type: 'sgv' }]);
const result = await find.findOne(col, docId.toString(), {});
observed.toArrayCalls.should.equal(1);
result.should.have.length(1);
result[0].should.have.property('identifier', docId.toString());
result[0].should.not.have.property('_id');
});
it('supports promise-based findOneFilter without normalization when requested', async function () {
const observed = {};
const docId = new ObjectId();
const col = createStubCollection(observed, [{ _id: docId, type: 'mbg' }]);
const result = await find.findOneFilter(col, { type: 'mbg' }, {}, { normalize: false });
observed.toArrayCalls.should.equal(1);
result.should.have.length(1);
result[0].should.have.property('_id');
result[0]._id.toString().should.equal(docId.toString());
});
});
+161
View File
@@ -0,0 +1,161 @@
'use strict';
require('should');
const { ObjectId } = require('mongodb');
const modify = require('../lib/api3/storage/mongoCollection/modify');
const MongoCollection = require('../lib/api3/storage/mongoCollection');
describe('API3 mongoCollection promise-based helpers', function () {
describe('modify helpers', function () {
it('insertOne uses promise-based collection methods and keeps existing behavior', async function () {
const doc = { _id: new ObjectId(), type: 'sgv' };
const insertedId = doc._id.toString();
const col = {
insertOne: function (receivedDoc) {
arguments.length.should.equal(1);
receivedDoc.should.equal(doc);
return Promise.resolve({ insertedId: receivedDoc._id });
}
};
const identifier = await modify.insertOne(col, doc);
identifier.should.equal(insertedId);
doc.should.not.have.property('_id');
});
it('replaceOne uses promise-based upsert without a callback', async function () {
const doc = { value: 42 };
const col = {
replaceOne: function (filter, receivedDoc, options) {
arguments.length.should.equal(3);
filter.should.eql({ $or: [{ identifier: 'record-1' }] });
receivedDoc.should.equal(doc);
options.should.eql({ upsert: true });
return Promise.resolve({ matchedCount: 1 });
}
};
const matchedCount = await modify.replaceOne(col, 'record-1', doc);
matchedCount.should.equal(1);
});
it('update and delete helpers use promise-based collection methods', async function () {
const col = {
updateOne: function (filter, update) {
arguments.length.should.equal(2);
filter.should.eql({ $or: [{ identifier: 'record-2' }] });
update.should.eql({ $set: { value: 84 } });
return Promise.resolve({ modifiedCount: 1 });
},
deleteOne: function (filter) {
arguments.length.should.equal(1);
filter.should.eql({ $or: [{ identifier: 'record-2' }] });
return Promise.resolve({ deletedCount: 1 });
},
deleteMany: function (filter) {
arguments.length.should.equal(1);
filter.should.eql({ $or: [{ kind: 'sgv' }, { kind: 'mbg' }] });
return Promise.resolve({ deletedCount: 2 });
}
};
const updated = await modify.updateOne(col, 'record-2', { value: 84 });
const deletedOne = await modify.deleteOne(col, 'record-2');
const deletedMany = await modify.deleteManyOr(col, [
{ field: 'kind', operator: 'eq', value: 'sgv' },
{ field: 'kind', operator: 'eq', value: 'mbg' }
]);
updated.should.eql({ updated: 1 });
deletedOne.should.eql({ deleted: 1 });
deletedMany.should.eql({ deleted: 2 });
});
});
describe('collection metadata helpers', function () {
function createCursor (observed, docs) {
return {
sort: function (spec) {
observed.sort = spec;
return this;
},
limit: function (value) {
observed.limit = value;
return this;
},
project: function (projection) {
observed.projection = projection;
return this;
},
toArray: function () {
observed.toArrayCalls = (observed.toArrayCalls || 0) + 1;
return Promise.resolve(docs);
}
};
}
function createStorage (observed, docs) {
const col = {
find: function () {
arguments.length.should.equal(0);
observed.findCalls = (observed.findCalls || 0) + 1;
return createCursor(observed, docs);
}
};
const ctx = {
store: {
collection: function (name) {
observed.collectionName = name;
return col;
},
ensureIndexes: function (receivedCol, fields) {
receivedCol.should.equal(col);
observed.indexFields = fields;
},
db: {
admin: function () {
return {
buildInfo: function () {
arguments.length.should.equal(0);
observed.buildInfoCalls = (observed.buildInfoCalls || 0) + 1;
return Promise.resolve({ version: '5.9.2' });
}
};
}
}
}
};
return new MongoCollection(ctx, {}, 'entries');
}
it('version uses promise-based buildInfo', async function () {
const observed = {};
const storage = createStorage(observed, []);
const version = await storage.version();
observed.collectionName.should.equal('entries');
observed.buildInfoCalls.should.equal(1);
version.should.eql({ storage: 'mongodb', version: '5.9.2' });
});
it('getLastModified uses promise-based cursor methods', async function () {
const observed = {};
const storage = createStorage(observed, [{ srvModified: 1234 }]);
const lastModified = await storage.getLastModified('srvModified');
observed.findCalls.should.equal(1);
observed.sort.should.eql({ srvModified: -1 });
observed.limit.should.equal(1);
observed.projection.should.eql({ srvModified: 1 });
observed.toArrayCalls.should.equal(1);
lastModified.should.eql({ srvModified: 1234 });
});
});
});
+2 -2
View File
@@ -12,7 +12,7 @@
* On mongodb driver 3.x (v15.0.6), Object.keys(new ObjectID()) returned
* ['_bsontype','id'], making _.isEmpty return false (correct).
*
* On mongodb driver 5.x / mongodb-legacy (dev), Object.keys(new ObjectId())
* On mongodb driver 5.x, Object.keys(new ObjectId())
* returns [], making _.isEmpty return true (incorrect treats valid
* ObjectId _id as empty).
*
@@ -21,7 +21,7 @@
*/
const _ = require('lodash');
const { ObjectId } = require('mongodb-legacy');
const { ObjectId } = require('mongodb');
const should = require('should');
describe('Cache ObjectId compatibility', function () {
+14 -14
View File
@@ -306,16 +306,12 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
describe('Simultaneous POST requests to entries', function () {
beforeEach(function (done) {
self.ctx.entries().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await self.ctx.entries().deleteMany({});
});
afterEach(function (done) {
self.ctx.entries().deleteMany({}, function () {
done();
});
afterEach(async function () {
await self.ctx.entries().deleteMany({});
});
it('handles 5 simultaneous single entry POSTs', function (done) {
@@ -422,9 +418,11 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
beforeEach(function (done) {
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
self.ctx.entries().deleteMany({}, function () {
done();
});
self.ctx.entries().deleteMany({})
.then(function () {
done();
})
.catch(done);
});
});
});
@@ -432,9 +430,11 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
afterEach(function (done) {
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
self.ctx.entries().deleteMany({}, function () {
done();
});
self.ctx.entries().deleteMany({})
.then(function () {
done();
})
.catch(done);
});
});
});
+66
View File
@@ -0,0 +1,66 @@
'use strict';
require('should');
const dataloaderInit = require('../lib/data/dataloader');
const createDData = require('../lib/data/ddata');
describe('dataloader', function () {
it('completes update when db.stats is promise-based', function (done) {
const ddata = createDData();
ddata.processTreatments = function () {};
const ctx = {
settings: {},
language: {
translate: function (value) { return value; }
},
cache: {
isEmpty: function () { return true; },
insertData: function (key, results) { return results; }
},
ddata: ddata,
entries: {
list: function (query, callback) { callback(null, []); }
},
treatments: {
list: function (query, callback) { callback(null, []); }
},
profile: {
last: function (callback) { callback(null, []); }
},
food: {
list: function (callback) { callback(null, []); }
},
devicestatus: {
list: function (query, callback) { callback(null, []); }
},
activity: {
list: function (query, callback) { callback(null, []); }
},
store: {
db: {
stats: function () {
return Promise.resolve({ dataSize: 123, indexSize: 456 });
}
}
}
};
const env = {
settings: {
isEnabled: function () { return false; },
units: 'mg/dl'
},
extendedSettings: {}
};
const loader = dataloaderInit(env, ctx);
loader.update(ddata, function (err) {
should.not.exist(err);
ddata.dbstats.should.eql({
dataSize: 123,
indexSize: 456
});
done();
});
});
});
+16 -2
View File
@@ -75,7 +75,14 @@ function guardedDeleteMany(collection, filter, callback) {
}
// Safe to proceed
return collection.deleteMany(filter, callback);
const promise = collection.deleteMany(filter);
if (callback) {
promise.then(
function onSuccess(result) { callback(null, result); },
function onError(err) { callback(err); }
);
}
return promise;
}
/**
@@ -95,7 +102,14 @@ function guardedDrop(collection, callback) {
throw err;
}
return collection.drop(callback);
const promise = collection.drop();
if (callback) {
promise.then(
function onSuccess(result) { callback(null, result); },
function onError(err) { callback(err); }
);
}
return promise;
}
/**
+14 -17
View File
@@ -63,10 +63,8 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
return self.ctx.store.collection(self.env.treatments_collection);
}
beforeEach(function (done) {
rawCollection().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await rawCollection().deleteMany({});
});
/**
@@ -75,16 +73,19 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
*/
function insertLegacyDoc (callback) {
var doc = Object.assign({}, LEGACY_OVERRIDE);
rawCollection().insertOne(doc, function (err) {
should.not.exist(err);
rawCollection().findOne({ _id: LEGACY_UUID }, function (err, stored) {
should.not.exist(err);
rawCollection().insertOne(doc)
.then(function () {
return rawCollection().findOne({ _id: LEGACY_UUID });
})
.then(function (stored) {
should.exist(stored, 'Legacy doc should exist after direct insert');
stored._id.should.equal(LEGACY_UUID);
should.not.exist(stored.identifier, 'Legacy doc must NOT have identifier field');
callback(stored);
})
.catch(function (err) {
should.not.exist(err);
});
});
}
describe('DELETE legacy UUID override via API', function () {
@@ -130,10 +131,8 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
// Check the database after the server has had time to process the upsert
setTimeout(function () {
rawCollection().find({ eventType: 'Temporary Override' }).toArray(function (err, docs) {
try {
should.not.exist(err);
rawCollection().find({ eventType: 'Temporary Override' }).toArray()
.then(function (docs) {
docs.length.should.equal(1,
'PUT should update the existing legacy override, not create a duplicate. '
+ 'Found ' + docs.length + ' documents. '
@@ -143,10 +142,8 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
);
done();
} catch (e) {
done(e);
}
});
})
.catch(done);
}, 5000);
});
});
+134
View File
@@ -0,0 +1,134 @@
'use strict';
var should = require('should');
var mongodb = require('mongodb');
describe('mongo storage retry lifecycle', function () {
var originalMongoClient = mongodb.MongoClient;
var originalSetTimeout = global.setTimeout;
function setMongoClient(fakeMongoClient) {
Object.defineProperty(mongodb, 'MongoClient', {
configurable: true,
enumerable: true,
value: fakeMongoClient,
writable: true
});
}
afterEach(function () {
setMongoClient(originalMongoClient);
global.setTimeout = originalSetTimeout;
delete require.cache[require.resolve('../lib/storage/mongo-storage')];
});
it('closes failed retry clients and calls back only after a successful retry', function (done) {
var createdClients = [];
var connectAttempts = 0;
var retryDelays = [];
function FakeMongoClient() {
this.closed = 0;
createdClients.push(this);
}
FakeMongoClient.prototype.on = function () {};
FakeMongoClient.prototype.connect = function () {
connectAttempts += 1;
if (connectAttempts === 1) {
var err = new Error('server selection failed');
err.name = 'MongoServerSelectionError';
return Promise.reject(err);
}
return Promise.resolve();
};
FakeMongoClient.prototype.db = function () {
return {
databaseName: 'testdb',
command: function () {
return Promise.resolve({ authInfo: { authenticatedUserRoles: [] } });
},
collection: function (name) {
return { collectionName: name };
}
};
};
FakeMongoClient.prototype.close = function () {
this.closed += 1;
return Promise.resolve();
};
setMongoClient(FakeMongoClient);
global.setTimeout = function (fn, ms) {
retryDelays.push(ms);
Promise.resolve().then(fn);
return 1;
};
delete require.cache[require.resolve('../lib/storage/mongo-storage')];
var store = require('../lib/storage/mongo-storage');
var callbackCount = 0;
store({ storageURI: 'mongodb://example/testdb' }, function (err, db) {
callbackCount += 1;
should.not.exist(err);
should.exist(db);
callbackCount.should.equal(1);
connectAttempts.should.equal(2);
createdClients.length.should.equal(2);
createdClients[0].closed.should.equal(1);
createdClients[1].closed.should.equal(0);
retryDelays.should.eql([3000]);
db.db.databaseName.should.equal('testdb');
done();
}, true);
});
it('closes the client and reports authentication failure once', function (done) {
var createdClients = [];
var retryDelays = [];
function FakeMongoClient() {
this.closed = 0;
createdClients.push(this);
}
FakeMongoClient.prototype.on = function () {};
FakeMongoClient.prototype.connect = function () {
return Promise.reject(new Error('AuthenticationFailed: bad auth'));
};
FakeMongoClient.prototype.close = function () {
this.closed += 1;
return Promise.resolve();
};
setMongoClient(FakeMongoClient);
global.setTimeout = function (fn, ms) {
retryDelays.push(ms);
Promise.resolve().then(fn);
return 1;
};
delete require.cache[require.resolve('../lib/storage/mongo-storage')];
var store = require('../lib/storage/mongo-storage');
var callbackCount = 0;
store({ storageURI: 'mongodb://example/testdb' }, function (err, db) {
callbackCount += 1;
should.exist(err);
should.not.exist(db);
err.message.should.equal('MongoDB authentication failed! Double check the URL has the right username and password in MONGODB_URI.');
callbackCount.should.equal(1);
createdClients.length.should.equal(1);
createdClients[0].closed.should.equal(1);
retryDelays.should.eql([]);
done();
}, true);
});
});
+36 -1
View File
@@ -30,6 +30,42 @@ describe('mongo storage', function () {
});
});
it('Uses the default database from the connection string via the public client API', function (done) {
var store = require('../lib/storage/mongo-storage');
store(env, function (err, db) {
should.not.exist(err);
should.exist(db.db);
should.exist(db.client);
db.client.db().databaseName.should.equal(db.db.databaseName);
db.db.databaseName.should.equal('testdb');
done();
});
});
it('ensureIndexes uses createIndex without the legacy background option', function (done) {
var store = require('../lib/storage/mongo-storage');
var calls = [];
store(env, function (err, db) {
should.not.exist(err);
db.ensureIndexes({
collectionName: 'entries',
createIndex: function (field, options) {
calls.push({ field: field, options: options });
return Promise.resolve();
}
}, ['date']);
calls.length.should.equal(1);
calls[0].field.should.equal('date');
should.not.exist(calls[0].options);
done();
});
});
it('When no connection-string is given the storage-class should throw an error.', function (done) {
delete env.storageURI;
should.not.exist(env.storageURI);
@@ -58,4 +94,3 @@ describe('mongo storage', function () {
});
});
+2 -2
View File
@@ -37,8 +37,8 @@ describe('Loop SGV Entry Upload Tests', function() {
});
});
beforeEach(function(done) {
self.ctx.entries().deleteMany({}, done);
beforeEach(async function() {
await self.ctx.entries().deleteMany({});
});
describe('TEST-SGV-001: Single SGV entry', function() {
+240 -103
View File
@@ -173,16 +173,12 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
describe('Entries Storage - lib/server/entries.js', function () {
beforeEach(function (done) {
self.ctx.entries().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await self.ctx.entries().deleteMany({});
});
afterEach(function (done) {
self.ctx.entries().deleteMany({}, function () {
done();
});
afterEach(async function () {
await self.ctx.entries().deleteMany({});
});
it('create() accepts single entry in array', function (done) {
@@ -218,16 +214,12 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
describe('Profile Storage - lib/server/profile.js', function () {
beforeEach(function (done) {
self.ctx.profile().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await self.ctx.profile().deleteMany({});
});
afterEach(function (done) {
self.ctx.profile().deleteMany({}, function () {
done();
});
afterEach(async function () {
await self.ctx.profile().deleteMany({});
});
it('create() accepts single profile object', function (done) {
@@ -347,13 +339,14 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
self.ctx.profile.save(updated, function (saveErr) {
should.not.exist(saveErr);
self.ctx.profile().find({ _id: savedId }).toArray(function (findErr, docs) {
should.not.exist(findErr);
docs.length.should.equal(1);
docs[0].store.Default.dia.should.equal(4);
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
});
self.ctx.profile().find({ _id: savedId }).toArray()
.then(function (docs) {
docs.length.should.equal(1);
docs[0].store.Default.dia.should.equal(4);
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
})
.catch(done);
});
});
});
@@ -438,28 +431,25 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
should.not.exist(err);
saved.created_at.should.equal('2020-01-01T00:00:00.000Z');
self.ctx.profile().find({ _id: saved._id }).toArray(function (findErr, docs) {
should.not.exist(findErr);
docs.length.should.equal(1);
docs[0].created_at.should.equal('2020-01-01T00:00:00.000Z');
done();
});
self.ctx.profile().find({ _id: saved._id }).toArray()
.then(function (docs) {
docs.length.should.equal(1);
docs[0].created_at.should.equal('2020-01-01T00:00:00.000Z');
done();
})
.catch(done);
});
});
});
describe('Food Storage - lib/server/food.js', function () {
beforeEach(function (done) {
self.ctx.food().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await self.ctx.food().deleteMany({});
});
afterEach(function (done) {
self.ctx.food().deleteMany({}, function () {
done();
});
afterEach(async function () {
await self.ctx.food().deleteMany({});
});
it('create() accepts single food object', function (done) {
@@ -480,20 +470,59 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
done();
});
});
it('save() updates an existing food by _id when created_at changes', function (done) {
self.ctx.food.create({
name: 'Test Food',
category: 'Test',
carbs: 20,
protein: 10,
fat: 5
}, function (err, createdDocs) {
should.not.exist(err);
should.exist(createdDocs);
createdDocs.should.be.an.Array();
createdDocs.length.should.equal(1);
var savedId = createdDocs[0]._id;
var updated = {
_id: savedId.toString(),
name: 'Updated Food',
category: 'Test',
carbs: 25,
protein: 10,
fat: 5,
created_at: '2024-10-26T21:32:49.173Z'
};
self.ctx.food.save(updated, function (saveErr, savedDocs) {
should.not.exist(saveErr);
should.exist(savedDocs);
savedDocs.should.be.an.Array();
savedDocs.length.should.equal(1);
self.ctx.food().find({ _id: savedId }).toArray()
.then(function (docs) {
docs.length.should.equal(1);
docs[0].name.should.equal('Updated Food');
docs[0].carbs.should.equal(25);
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
})
.catch(done);
});
});
});
});
describe('Activity Storage - lib/server/activity.js', function () {
beforeEach(function (done) {
self.ctx.activity().deleteMany({}, function () {
done();
});
beforeEach(async function () {
await self.ctx.activity().deleteMany({});
});
afterEach(function (done) {
self.ctx.activity().deleteMany({}, function () {
done();
});
afterEach(async function () {
await self.ctx.activity().deleteMany({});
});
it('create() accepts array of activity objects (single object not supported)', function (done) {
@@ -511,6 +540,124 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
done();
});
});
it('save() updates an existing activity by _id', function (done) {
self.ctx.activity.create([{
created_at: '2024-10-26T20:32:49.173Z',
heartrate: 80,
steps: 100,
activitylevel: 'walking'
}], function (err, createdDocs) {
should.not.exist(err);
should.exist(createdDocs);
createdDocs.length.should.equal(1);
should.exist(createdDocs[0]._id);
var savedId = createdDocs[0]._id;
var updated = {
_id: savedId.toString(),
created_at: '2024-10-26T21:32:49.173Z',
heartrate: 95,
steps: 250,
activitylevel: 'running'
};
self.ctx.activity.save(updated, function (saveErr, savedDoc) {
should.not.exist(saveErr);
should.exist(savedDoc);
savedDoc._id.toString().should.equal(savedId.toString());
self.ctx.activity().find({ _id: savedId }).toArray()
.then(function (docs) {
docs.length.should.equal(1);
docs[0].heartrate.should.equal(95);
docs[0].steps.should.equal(250);
docs[0].activitylevel.should.equal('running');
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
})
.catch(done);
});
});
});
});
describe('Authorization Storage - lib/authorization/storage.js', function () {
function rolesCollection() {
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'roles');
}
function subjectsCollection() {
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'subjects');
}
beforeEach(async function () {
await rolesCollection().deleteMany({ name: 'mongo-save-role' });
await subjectsCollection().deleteMany({ name: 'mongo-save-subject' });
});
afterEach(async function () {
await rolesCollection().deleteMany({ name: 'mongo-save-role' });
await subjectsCollection().deleteMany({ name: 'mongo-save-subject' });
});
it('saveRole() updates an existing role without duplicating it', function (done) {
rolesCollection().insertOne({
name: 'mongo-save-role',
permissions: ['api:entries:read'],
notes: 'original',
created_at: '2024-10-26T20:32:49.173Z'
}).then(function (result) {
self.ctx.authorization.storage.saveRole({
_id: result.insertedId.toString(),
name: 'mongo-save-role',
permissions: ['api:entries:update'],
notes: 'updated',
created_at: '2024-10-26T21:32:49.173Z'
}, function (saveErr) {
should.not.exist(saveErr);
rolesCollection().find({ name: 'mongo-save-role' }).toArray()
.then(function (docs) {
docs.length.should.equal(1);
docs[0].permissions.should.deepEqual(['api:entries:update']);
docs[0].notes.should.equal('updated');
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
})
.catch(done);
});
}).catch(done);
});
it('saveSubject() updates an existing subject without duplicating it', function (done) {
subjectsCollection().insertOne({
name: 'mongo-save-subject',
roles: ['readable'],
notes: 'original',
created_at: '2024-10-26T20:32:49.173Z'
}).then(function (result) {
self.ctx.authorization.storage.saveSubject({
_id: result.insertedId.toString(),
name: 'mongo-save-subject',
roles: ['admin'],
notes: 'updated',
created_at: '2024-10-26T21:32:49.173Z'
}, function (saveErr) {
should.not.exist(saveErr);
subjectsCollection().find({ name: 'mongo-save-subject' }).toArray()
.then(function (docs) {
docs.length.should.equal(1);
docs[0].roles.should.deepEqual(['admin']);
docs[0].notes.should.equal('updated');
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
done();
})
.catch(done);
});
}).catch(done);
});
});
});
@@ -533,71 +680,61 @@ describe('MongoDB insertOne vs insertMany Behavior', function () {
describe('Direct MongoDB operations - testing insertOne with array data', function () {
it('insertOne with object inserts correctly', function (done) {
it('insertOne with object inserts correctly', async function () {
var testCollection = self.ctx.store.collection('test_shape_handling');
testCollection.deleteMany({}, function () {
testCollection.insertOne({ type: 'test', value: 42 }, function (err, result) {
should.not.exist(err);
should.exist(result);
result.insertedId.should.be.ok();
testCollection.find({}).toArray(function (err, docs) {
docs.length.should.equal(1);
docs[0].value.should.equal(42);
testCollection.deleteMany({}, done);
});
});
});
await testCollection.deleteMany({});
var result = await testCollection.insertOne({ type: 'test', value: 42 });
should.exist(result);
result.insertedId.should.be.ok();
var docs = await testCollection.find({}).toArray();
docs.length.should.equal(1);
docs[0].value.should.equal(42);
await testCollection.deleteMany({});
});
it('insertOne with array creates single document containing array (NOT multiple docs)', function (done) {
it('insertOne with array creates single document containing array (NOT multiple docs)', async function () {
var testCollection = self.ctx.store.collection('test_shape_handling');
testCollection.deleteMany({}, function () {
var arrayData = [
{ type: 'test', value: 1 },
{ type: 'test', value: 2 },
{ type: 'test', value: 3 }
];
testCollection.insertOne(arrayData, function (err, result) {
if (err) {
console.log('insertOne with array error:', err.message);
done();
} else {
testCollection.find({}).toArray(function (err, docs) {
console.log('Documents after insertOne with array:', JSON.stringify(docs, null, 2));
console.log('Number of documents:', docs.length);
testCollection.deleteMany({}, done);
});
}
});
});
await testCollection.deleteMany({});
var arrayData = [
{ type: 'test', value: 1 },
{ type: 'test', value: 2 },
{ type: 'test', value: 3 }
];
try {
await testCollection.insertOne(arrayData);
} catch (err) {
console.log('insertOne with array error:', err.message);
await testCollection.deleteMany({});
return;
}
var docs = await testCollection.find({}).toArray();
console.log('Documents after insertOne with array:', JSON.stringify(docs, null, 2));
console.log('Number of documents:', docs.length);
await testCollection.deleteMany({});
});
it('insertMany with array creates multiple documents', function (done) {
it('insertMany with array creates multiple documents', async function () {
var testCollection = self.ctx.store.collection('test_shape_handling');
testCollection.deleteMany({}, function () {
var arrayData = [
{ type: 'test', value: 1 },
{ type: 'test', value: 2 },
{ type: 'test', value: 3 }
];
testCollection.insertMany(arrayData, function (err, result) {
should.not.exist(err);
should.exist(result);
result.insertedCount.should.equal(3);
testCollection.find({}).toArray(function (err, docs) {
docs.length.should.equal(3);
testCollection.deleteMany({}, done);
});
});
});
await testCollection.deleteMany({});
var arrayData = [
{ type: 'test', value: 1 },
{ type: 'test', value: 2 },
{ type: 'test', value: 3 }
];
var result = await testCollection.insertMany(arrayData);
should.exist(result);
result.insertedCount.should.equal(3);
var docs = await testCollection.find({}).toArray();
docs.length.should.equal(3);
await testCollection.deleteMany({});
});
});
});
+1 -2
View File
@@ -12,6 +12,7 @@
var request = require('supertest');
var should = require('should');
var ObjectID = require('mongodb').ObjectId;
var language = require('../lib/language')();
var api = require('../lib/api/');
@@ -243,7 +244,6 @@ describe('UUID_HANDLING=true', function() {
});
it('UUID-ON-003: ObjectId still works normally', function(done) {
var ObjectID = require('mongodb').ObjectId;
var testId = new ObjectID();
// Insert treatment with ObjectId
@@ -491,7 +491,6 @@ describe('UUID Edge Cases', function() {
});
it('UUID-EDGE-007: Valid ObjectId still works normally', function(done) {
var ObjectID = require('mongodb').ObjectId;
var testId = new ObjectID();
self.ctx.treatments.create([{
+147 -15
View File
@@ -79,6 +79,10 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
return self.ctx.store.collection(self.env.treatments_collection);
}
function foodCollection() {
return self.ctx.food();
}
describe('dbAdd with treatments collection', function () {
beforeEach(function (done) {
@@ -278,8 +282,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
eventType: 'Note',
created_at: createdAt,
notes: 'legacy original'
}, function (insertErr) {
if (insertErr) return done(insertErr);
}).then(function () {
socket.emit('dbUpdate', {
collection: 'treatments',
@@ -293,7 +296,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
waitForConditionWithWarning({
condition: function (cb) {
treatmentsCollection().findOne({ _id: legacyId }, cb);
treatmentsCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.exist(doc);
@@ -303,7 +308,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
operationName: 'verify websocket dbUpdate with custom string _id'
});
});
});
}).catch(done);
});
});
});
@@ -328,8 +333,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
eventType: 'Note',
created_at: createdAt,
notes: 'remove me'
}, function (insertErr) {
if (insertErr) return done(insertErr);
}).then(function () {
socket.emit('dbUpdateUnset', {
collection: 'treatments',
@@ -343,7 +347,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
waitForConditionWithWarning({
condition: function (cb) {
treatmentsCollection().findOne({ _id: legacyId }, cb);
treatmentsCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.exist(doc);
@@ -353,7 +359,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
operationName: 'verify websocket dbUpdateUnset with custom string _id'
});
});
});
}).catch(done);
});
});
});
@@ -408,8 +414,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
eventType: 'Note',
created_at: createdAt,
notes: 'delete me'
}, function (insertErr) {
if (insertErr) return done(insertErr);
}).then(function () {
socket.emit('dbRemove', {
collection: 'treatments',
@@ -420,7 +425,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
waitForConditionWithWarning({
condition: function (cb) {
treatmentsCollection().findOne({ _id: legacyId }, cb);
treatmentsCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.not.exist(doc);
@@ -429,7 +436,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
operationName: 'verify websocket dbRemove with custom string _id'
});
});
});
}).catch(done);
});
});
});
@@ -455,8 +462,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
eventType: 'Note',
created_at: originalCreatedAt,
notes: 'existing legacy note'
}, function (insertErr) {
if (insertErr) return done(insertErr);
}).then(function () {
socket.emit('dbAdd', {
collection: 'treatments',
@@ -473,7 +479,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
waitForConditionWithWarning({
condition: function (cb) {
treatmentsCollection().findOne({ _id: legacyId }, cb);
treatmentsCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.exist(doc);
@@ -483,8 +491,132 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
operationName: 'verify websocket dbAdd dedupe with custom string _id'
});
});
}).catch(done);
});
});
});
describe('generic collection raw write watchpoints', function () {
beforeEach(async function () {
await foodCollection().deleteMany({});
});
it('dbAdd preserves custom string _id values for generic collections', function (done) {
connectAndAuthorize(function (err, socket, authResult) {
if (err) return done(err);
authResult.write.should.equal(true);
var legacyId = 'legacy-food-id-add';
socket.emit('dbAdd', {
collection: 'food',
data: {
_id: legacyId,
name: 'ws food',
carbs: 15
}
}, function (result) {
should.exist(result);
result.should.be.instanceof(Array);
result.length.should.equal(1);
result[0]._id.should.equal(legacyId);
waitForConditionWithWarning({
condition: function (cb) {
foodCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.exist(doc);
doc.name.should.equal('ws food');
doc.carbs.should.equal(15);
},
done: done,
operationName: 'verify websocket dbAdd generic collection custom string _id'
});
});
});
});
it('dbUpdate supports custom string _id values for generic collections', function (done) {
connectAndAuthorize(function (err, socket, authResult) {
if (err) return done(err);
authResult.write.should.equal(true);
var legacyId = 'legacy-food-id-update';
foodCollection().insertOne({
_id: legacyId,
name: 'original food',
carbs: 10,
protein: 2
}).then(function () {
socket.emit('dbUpdate', {
collection: 'food',
_id: legacyId,
data: {
carbs: 18,
protein: 4
}
}, function (updateResult) {
should.exist(updateResult);
updateResult.result.should.equal('success');
waitForConditionWithWarning({
condition: function (cb) {
foodCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.exist(doc);
doc.carbs.should.equal(18);
doc.protein.should.equal(4);
},
done: done,
operationName: 'verify websocket dbUpdate generic collection custom string _id'
});
});
}).catch(done);
});
});
it('dbRemove supports custom string _id values for generic collections', function (done) {
connectAndAuthorize(function (err, socket, authResult) {
if (err) return done(err);
authResult.write.should.equal(true);
var legacyId = 'legacy-food-id-remove';
foodCollection().insertOne({
_id: legacyId,
name: 'remove food',
carbs: 9
}).then(function () {
socket.emit('dbRemove', {
collection: 'food',
_id: legacyId
}, function (removeResult) {
should.exist(removeResult);
removeResult.result.should.equal('success');
waitForConditionWithWarning({
condition: function (cb) {
foodCollection().findOne({ _id: legacyId })
.then(function (doc) { cb(null, doc); })
.catch(cb);
},
assertion: function (doc) {
should.not.exist(doc);
},
done: done,
operationName: 'verify websocket dbRemove generic collection custom string _id'
});
});
}).catch(done);
});
});
});
});