diff --git a/lib/api3/generic/collection.js b/lib/api3/generic/collection.js index 015af2a1..2ec81087 100644 --- a/lib/api3/generic/collection.js +++ b/lib/api3/generic/collection.js @@ -76,12 +76,12 @@ function Collection ({ ctx, env, app, colName, storageColName, fallbackGetDate, * Parse limit (max document count) from query string */ self.parseLimit = function parseLimit (req, res) { - const maxLimit = app.get('API3_MAX_LIMIT'); + const maxLimit = parseInt(app.get('API3_MAX_LIMIT'), 10) || apiConst.API3_MAX_LIMIT; let limit = maxLimit; if (req.query.limit) { if (!isNaN(req.query.limit) && req.query.limit > 0 && req.query.limit <= maxLimit) { - limit = parseInt(req.query.limit); + limit = parseInt(req.query.limit, 10); } else { opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LIMIT); diff --git a/lib/api3/generic/patch/operation.js b/lib/api3/generic/patch/operation.js index dff18c70..45ca3ad4 100644 --- a/lib/api3/generic/patch/operation.js +++ b/lib/api3/generic/patch/operation.js @@ -6,7 +6,7 @@ const _ = require('lodash') , validate = require('./validate.js') , opTools = require('../../shared/operationTools') , dateTools = require('../../shared/dateTools') - , FieldsProjector = require('../../shared/fieldsProjector') + , treatmentDuration = require('../../../treatmentDuration') ; /** @@ -78,6 +78,8 @@ async function applyPatch (opCtx, identifier, doc, storageDoc) { doc.modifiedBy = auth.subject.name; } + treatmentDuration.normalizeTreatmentDuration(doc, storageDoc); + const matchedCount = await col.storage.updateOne(identifier, doc); if (!matchedCount) @@ -86,10 +88,7 @@ async function applyPatch (opCtx, identifier, doc, storageDoc) { res.setHeader('Last-Modified', now.toUTCString()); opTools.sendJSONStatus(res, apiConst.HTTP.OK); - const fieldsProjector = new FieldsProjector('_all'); - const patchedDocs = await col.storage.findOne(identifier, fieldsProjector); - const patchedDoc = patchedDocs[0]; - fieldsProjector.applyProjection(patchedDoc); + const patchedDoc = Object.assign({}, storageDoc, doc); ctx.bus.emit('storage-socket-update', { colName: col.colName, doc: patchedDoc }); col.autoPrune(); diff --git a/lib/api3/generic/search/input.js b/lib/api3/generic/search/input.js index dbd37356..a3bbfcf8 100644 --- a/lib/api3/generic/search/input.js +++ b/lib/api3/generic/search/input.js @@ -121,7 +121,7 @@ function parseSkip (req, res) { if (req.query.skip) { if (!isNaN(req.query.skip) && req.query.skip >= 0) { - skip = parseInt(req.query.skip); + skip = parseInt(req.query.skip, 10); } else { opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_SKIP); @@ -137,4 +137,4 @@ module.exports = { parseFilter, parseSort, parseSkip -}; \ No newline at end of file +}; diff --git a/lib/api3/generic/update/replace.js b/lib/api3/generic/update/replace.js index c0c76c4b..82a0de9e 100644 --- a/lib/api3/generic/update/replace.js +++ b/lib/api3/generic/update/replace.js @@ -5,6 +5,7 @@ const apiConst = require('../../const.json') , validate = require('./validate.js') , path = require('path') , opTools = require('../../shared/operationTools') + , treatmentDuration = require('../../../treatmentDuration') ; /** @@ -32,6 +33,8 @@ async function replace (opCtx, doc, storageDoc, options) { doc.subject = auth.subject.name; } + treatmentDuration.normalizeTreatmentDuration(doc); + const matchedCount = await col.storage.replaceOne(storageDoc.identifier, doc); if (!matchedCount) diff --git a/lib/api3/storage/mongoCollection/find.js b/lib/api3/storage/mongoCollection/find.js index 013d008e..aedcc841 100644 --- a/lib/api3/storage/mongoCollection/find.js +++ b/lib/api3/storage/mongoCollection/find.js @@ -4,6 +4,18 @@ const utils = require('./utils') , _ = require('lodash') ; +/** + * Ensure Mongo limit/skip receive integers even when callers pass env strings. + */ +function toSafeInt (value, defaultValue) { + if (value === null || value === undefined) { + return defaultValue; + } + + const parsed = parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : defaultValue; +} + /** * Find single document by identifier @@ -73,11 +85,13 @@ function findMany (col, args) { return new Promise(function (resolve, reject) { const filter = utils.parseFilter(args.filter, logicalOperator, args.onlyValid); + const safeLimit = toSafeInt(args.limit, 1000); + const safeSkip = toSafeInt(args.skip, 0); col.find(filter) .sort(args.sort) - .limit(args.limit) - .skip(args.skip) + .limit(safeLimit) + .skip(safeSkip) .project(args.projection) .toArray(function mongoDone (err, result) { diff --git a/lib/data/ddata.js b/lib/data/ddata.js index 0ee1d9eb..6cddf943 100644 --- a/lib/data/ddata.js +++ b/lib/data/ddata.js @@ -43,6 +43,27 @@ function init () { && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { obj[key].mills = new Date(obj[key].sysTime).getTime(); } + if (Object.prototype.hasOwnProperty.call(obj[key], 'durationInMilliseconds') + && !Object.prototype.hasOwnProperty.call(obj[key], 'duration')) { + var durationInMilliseconds = Number(obj[key].durationInMilliseconds) || 0; + if (durationInMilliseconds > 0) { + obj[key].duration = Math.round(durationInMilliseconds / 60000); + } + } + if (!Object.prototype.hasOwnProperty.call(obj[key], 'endmills') || obj[key].endmills == null) { + var baseMills = Number(obj[key].mills); + if (Number.isFinite(baseMills)) { + obj[key].mills = baseMills; + if (Object.prototype.hasOwnProperty.call(obj[key], 'durationInMilliseconds')) { + var endmillsDuration = Number(obj[key].durationInMilliseconds) || 0; + if (endmillsDuration > 0) { + obj[key].endmills = baseMills + endmillsDuration; + } + } else if (Object.prototype.hasOwnProperty.call(obj[key], 'duration')) { + obj[key].endmills = baseMills + times.mins(Number(obj[key].duration) || 0).msecs; + } + } + } } }); @@ -65,7 +86,8 @@ function init () { const oldElement = oldData[i]; let found = false; for (let j = 0; j < newData.length; j++) { - if (oldElement._id == newData[j]._id) { + if ((oldElement._id && oldElement._id == newData[j]._id) + || (oldElement.identifier && oldElement.identifier === newData[j].identifier)) { found = true; break; } diff --git a/lib/server/websocket.js b/lib/server/websocket.js index ebb04b40..e5eb5951 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -10,6 +10,20 @@ function getRemoteIP (req) { return address.ip; } +// Only coerce canonical 24-char hex strings to ObjectId. +// Preserve custom string ids and existing ObjectId instances. +function safeObjectID (id) { + if (id instanceof ObjectID) { + return id; + } + + if (typeof id === 'string' && /^[0-9a-fA-F]{24}$/.test(id)) { + return new ObjectID(id); + } + + return id; +} + function init (env, ctx, server) { function websocket () { @@ -214,13 +228,7 @@ function init (env, ctx, server) { } return; } - var id; - try { - id = new ObjectID(data._id); - } catch (err) { - console.error(err); - id = new ObjectID(); - } + var id = safeObjectID(data._id); ctx.store.collection(collection).updateOne({ '_id': id } , { $set: data.data } @@ -269,7 +277,7 @@ function init (env, ctx, server) { return; } - var objId = new ObjectID(data._id); + var objId = safeObjectID(data._id); ctx.store.collection(collection).updateOne({ '_id': objId }, { $unset: data.data } , function(err, results) { @@ -434,7 +442,7 @@ function init (env, ctx, server) { if (array.length > 0) { console.log(LOG_DEDUP + 'Found similiar', array[0]); array[0].created_at = data.data.created_at; - var objId = new ObjectID(array[0]._id); + var objId = safeObjectID(array[0]._id); ctx.store.collection(collection).updateOne({ '_id': objId }, { $set: { created_at: data.data.created_at } }); if (callback) { callback([array[0]]); @@ -553,7 +561,7 @@ function init (env, ctx, server) { return; } - var objId = new ObjectID(data._id); + var objId = safeObjectID(data._id); ctx.store.collection(collection).deleteOne({ '_id': objId } , function(err, stat) { diff --git a/lib/treatmentDuration.js b/lib/treatmentDuration.js new file mode 100644 index 00000000..5916a18f --- /dev/null +++ b/lib/treatmentDuration.js @@ -0,0 +1,73 @@ +'use strict'; + +const times = require('./times'); + +function hasOwnProperty (obj, field) { + return obj && Object.prototype.hasOwnProperty.call(obj, field); +} + +function toMills (value) { + if (value === null || typeof value === 'undefined') { + return null; + } + + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null; + } + + if (typeof value === 'string' && value.trim() === '') { + return null; + } + + const numeric = Number(value); + if (Number.isFinite(numeric)) { + return numeric; + } + + const dateMills = new Date(value).getTime(); + return Number.isFinite(dateMills) ? dateMills : null; +} + +function resolveBaseMills (doc, fallbackDoc) { + return toMills(doc && doc.mills) + ?? toMills(fallbackDoc && fallbackDoc.mills) + ?? toMills(doc && doc.created_at) + ?? toMills(doc && doc.date) + ?? toMills(fallbackDoc && fallbackDoc.created_at) + ?? toMills(fallbackDoc && fallbackDoc.date); +} + +function normalizeTreatmentDuration (doc, fallbackDoc) { + const baseMills = resolveBaseMills(doc, fallbackDoc); + + if ((!hasOwnProperty(doc, 'endmills') || doc.endmills == null) && baseMills !== null) { + if (hasOwnProperty(doc, 'durationInMilliseconds')) { + const durationInMilliseconds = Number(doc.durationInMilliseconds) || 0; + if (durationInMilliseconds > 0) { + doc.endmills = baseMills + durationInMilliseconds; + } + } else if (hasOwnProperty(doc, 'duration')) { + doc.endmills = baseMills + times.mins(Number(doc.duration) || 0).msecs; + } else if (hasOwnProperty(fallbackDoc, 'durationInMilliseconds')) { + const durationInMilliseconds = Number(fallbackDoc.durationInMilliseconds) || 0; + if (durationInMilliseconds > 0) { + doc.endmills = baseMills + durationInMilliseconds; + } + } else if (hasOwnProperty(fallbackDoc, 'duration')) { + doc.endmills = baseMills + times.mins(Number(fallbackDoc.duration) || 0).msecs; + } + } + + const endMills = hasOwnProperty(doc, 'endmills') ? Number(doc.endmills) : NaN; + if (Number.isFinite(baseMills) && Number.isFinite(endMills) && endMills >= baseMills) { + doc.durationInMilliseconds = endMills - baseMills; + doc.duration = Math.round(doc.durationInMilliseconds / 60000); + } + + return doc; +} + +module.exports = { + normalizeTreatmentDuration, + resolveBaseMills +}; diff --git a/tests/api3.patch.operation.test.js b/tests/api3.patch.operation.test.js new file mode 100644 index 00000000..2fd9f10f --- /dev/null +++ b/tests/api3.patch.operation.test.js @@ -0,0 +1,114 @@ +'use strict'; + +require('should'); + +describe('API3 PATCH operation', function () { + const should = require('should'); + const security = require('../lib/api3/security'); + const patchOperation = require('../lib/api3/generic/patch/operation'); + + let originalAuthenticate; + let originalDemandPermission; + + beforeEach(() => { + originalAuthenticate = security.authenticate; + originalDemandPermission = security.demandPermission; + }); + + afterEach(() => { + security.authenticate = originalAuthenticate; + security.demandPermission = originalDemandPermission; + }); + + it('emits the merged patched document without re-reading storage', async () => { + const date = 1741255200000; + const storageDoc = { + identifier: 'test-patch-operation', + date: date, + utcOffset: 0, + app: 'nightscout-test', + device: 'ns://pump', + eventType: 'Temp Basal', + absolute: 1.2, + duration: 30, + srvModified: date - 1000 + }; + const patchDoc = { + absolute: 0.7, + duration: 0, + durationInMilliseconds: 26584 + }; + const events = []; + + security.authenticate = async () => ({ subject: { name: 'patch-user' } }); + security.demandPermission = async () => true; + + const col = { + colName: 'treatments', + storage: { + identifyingFilter: identifier => ({ identifier }), + findOneFilter: async () => [Object.assign({}, storageDoc)], + updateOne: async () => ({ updated: 1 }), + findOne: async () => { + throw new Error('PATCH should not re-read the document after update'); + } + }, + resolveDates: doc => new Date(doc.srvModified), + autoPrune: () => { + events.push({ name: 'auto-prune' }); + } + }; + const ctx = { + bus: { + emit: (name, payload) => { + events.push({ name, payload }); + } + } + }; + const req = { + body: Object.assign({}, patchDoc), + params: { identifier: storageDoc.identifier }, + get: () => null + }; + const res = createResponse(); + const handler = patchOperation(ctx, {}, {}, col); + + await handler(req, res); + + res.statusCode.should.equal(200); + should.exist(res.headers['Last-Modified']); + + const updateEvent = events.find(event => event.name === 'storage-socket-update'); + should.exist(updateEvent); + updateEvent.payload.colName.should.equal('treatments'); + updateEvent.payload.doc.should.containEql({ + identifier: storageDoc.identifier, + absolute: 0.7, + duration: 0, + durationInMilliseconds: 26584, + modifiedBy: 'patch-user' + }); + updateEvent.payload.doc.endmills.should.equal(date + 26584); + }); +}); + +function createResponse () { + return { + headers: {}, + headersSent: false, + statusCode: null, + body: null, + setHeader (name, value) { + this.headers[name] = value; + }, + status (code) { + this.statusCode = code; + return this; + }, + json (body) { + this.body = body; + this.headersSent = true; + return this; + } + }; +} diff --git a/tests/api3.patch.test.js b/tests/api3.patch.test.js index 3e729d2d..2f4c9070 100644 --- a/tests/api3.patch.test.js +++ b/tests/api3.patch.test.js @@ -235,5 +235,44 @@ describe('API3 PATCH', function() { self.cache.nextShouldEql(self.col, body) }); -}); + it('should normalize endmills when patching durationInMilliseconds', async () => { + const tempBasalDoc = { + date: self.validDoc.date + 1, + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 PATCH duration', + eventType: 'Temp Basal', + absolute: 1.2, + duration: 30 + }; + tempBasalDoc.identifier = opTools.calculateIdentifier(tempBasalDoc); + + let res = await self.instance.post(`${self.url}`, self.jwt.create) + .send(tempBasalDoc) + .expect(201); + + res.body.status.should.equal(201); + self.cache.nextShouldEql(self.col, tempBasalDoc) + + res = await self.instance.patch(`${self.url}/${tempBasalDoc.identifier}`, self.jwt.update) + .send({ + absolute: 0.7, + duration: 0, + durationInMilliseconds: 26584 + }) + .expect(200); + + res.body.status.should.equal(200); + + const body = await self.get(tempBasalDoc.identifier); + body.absolute.should.equal(0.7); + body.duration.should.equal(0); + body.durationInMilliseconds.should.equal(26584); + body.endmills.should.equal(tempBasalDoc.date + 26584); + body.modifiedBy.should.equal(self.subject.apiUpdate.name); + + self.cache.nextShouldEql(self.col, body) + }); + +}); diff --git a/tests/api3.search.test.js b/tests/api3.search.test.js index aeafc846..1e671923 100644 --- a/tests/api3.search.test.js +++ b/tests/api3.search.test.js @@ -277,5 +277,17 @@ describe('API3 SEARCH', function() { apiApp.set('API3_MAX_LIMIT', limitBackup); }); -}); + it('should respect string API3_MAX_LIMIT defaults', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', '5'); + let res = await self.instance.get(`${self.url}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.equal(5); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + +}); diff --git a/tests/api3.storage.find.test.js b/tests/api3.storage.find.test.js new file mode 100644 index 00000000..4aa16b83 --- /dev/null +++ b/tests/api3.storage.find.test.js @@ -0,0 +1,66 @@ +'use strict'; + +require('should'); + +const find = require('../lib/api3/storage/mongoCollection/find'); + +describe('API3 mongoCollection findMany', function () { + function createStubCollection (observed) { + return { + find: function () { + return this; + }, + sort: function () { + return this; + }, + limit: function (value) { + observed.limit = value; + return this; + }, + skip: function (value) { + observed.skip = value; + return this; + }, + project: function () { + return this; + }, + toArray: function (callback) { + callback(null, []); + } + }; + } + + it('coerces string limit and skip before calling Mongo', async function () { + const observed = {}; + const col = createStubCollection(observed); + + const result = await find.findMany(col, { + filter: [], + sort: { created_at: -1 }, + limit: '5', + skip: '2', + projection: {} + }); + + observed.limit.should.equal(5); + observed.skip.should.equal(2); + result.should.eql([]); + }); + + + it('coerces float-like values to integers before calling Mongo', async function () { + const observed = {}; + const col = createStubCollection(observed); + + await find.findMany(col, { + filter: [], + sort: { created_at: -1 }, + limit: 5.9, + skip: 2.4, + projection: {} + }); + + observed.limit.should.equal(5); + observed.skip.should.equal(2); + }); +}); diff --git a/tests/api3.update.test.js b/tests/api3.update.test.js index 14f4fb87..68931df0 100644 --- a/tests/api3.update.test.js +++ b/tests/api3.update.test.js @@ -297,6 +297,47 @@ describe('API3 UPDATE', function() { }); + it('should normalize endmills when replacing durationInMilliseconds', async () => { + const tempBasalDoc = { + identifier: utils.randomString('32', 'aA#'), + date: (new Date()).getTime() + 1, + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 UPDATE duration', + eventType: 'Temp Basal', + absolute: 1.2, + duration: 30 + }; + + let res = await self.instance.put(`${self.url}/${tempBasalDoc.identifier}`, self.jwt.all) + .send(tempBasalDoc) + .expect(201); + + res.body.status.should.equal(201); + self.cache.nextShouldEql(self.col, tempBasalDoc) + + const replacedDoc = Object.assign({}, tempBasalDoc, { + absolute: 0.7, + duration: 0, + durationInMilliseconds: 26584 + }); + + res = await self.instance.put(`${self.url}/${tempBasalDoc.identifier}`, self.jwt.update) + .send(replacedDoc) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldEql(self.col, replacedDoc) + + const body = await self.get(tempBasalDoc.identifier); + body.absolute.should.equal(0.7); + body.duration.should.equal(0); + body.durationInMilliseconds.should.equal(26584); + body.endmills.should.equal(tempBasalDoc.date + 26584); + body.subject.should.equal(self.subject.apiUpdate.name); + }); + + it('should not update deleted document', async () => { let res = await self.instance.delete(self.urlIdent, self.jwt.delete) .expect(200); @@ -312,4 +353,3 @@ describe('API3 UPDATE', function() { }); }); - diff --git a/tests/ddata.test.js b/tests/ddata.test.js index 034847b8..a6e9f3c8 100644 --- a/tests/ddata.test.js +++ b/tests/ddata.test.js @@ -41,6 +41,32 @@ describe('ddata', function ( ) { done( ); }); + it('processRawDataForRuntime derives duration and endmills from durationInMilliseconds', function () { + var ddata = require('../lib/data/ddata')(); + var createdAt = '2026-03-06T10:00:00.000Z'; + var result = ddata.processRawDataForRuntime([{ + _id: '507f1f77bcf86cd799439011', + created_at: createdAt, + durationInMilliseconds: 26584 + }])[0]; + + result.mills.should.equal(new Date(createdAt).getTime()); + result.duration.should.equal(0); + result.endmills.should.equal(result.mills + 26584); + }); + + it('idMergePreferNew matches records by identifier when _id is missing', function () { + var ddata = require('../lib/data/ddata')(); + var merged = ddata.idMergePreferNew( + [{ _id: 'mongo-id', identifier: 'loop-id', carbs: 15 }], + [{ identifier: 'loop-id', carbs: 0 }] + ); + + merged.length.should.equal(1); + merged[0].carbs.should.equal(0); + merged[0].identifier.should.equal('loop-id'); + }); + // TODO: ensure partition function gets called via: // Properties // * ddata.devicestatus @@ -57,4 +83,3 @@ describe('ddata', function ( ) { }); - diff --git a/tests/websocket.shape-handling.test.js b/tests/websocket.shape-handling.test.js index ffb64826..ee49d0a8 100644 --- a/tests/websocket.shape-handling.test.js +++ b/tests/websocket.shape-handling.test.js @@ -75,6 +75,10 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () { }); } + function treatmentsCollection() { + return self.ctx.store.collection(self.env.treatments_collection); + } + describe('dbAdd with treatments collection', function () { beforeEach(function (done) { @@ -261,6 +265,97 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () { }); }); }); + + it('dbUpdate supports custom string _id values', function (done) { + connectAndAuthorize(function (err, socket) { + if (err) return done(err); + + var legacyId = 'legacy-string-id-update'; + var createdAt = new Date().toISOString(); + + treatmentsCollection().insertOne({ + _id: legacyId, + eventType: 'Note', + created_at: createdAt, + notes: 'legacy original' + }, function (insertErr) { + if (insertErr) return done(insertErr); + + socket.emit('dbUpdate', { + collection: 'treatments', + _id: legacyId, + data: { + notes: 'legacy updated' + } + }, function (updateResult) { + should.exist(updateResult); + updateResult.result.should.equal('success'); + + waitForConditionWithWarning({ + condition: function (cb) { + treatmentsCollection().findOne({ _id: legacyId }, cb); + }, + assertion: function (doc) { + should.exist(doc); + doc.notes.should.equal('legacy updated'); + }, + done: done, + operationName: 'verify websocket dbUpdate with custom string _id' + }); + }); + }); + }); + }); + }); + + describe('dbUpdateUnset operations', function () { + + beforeEach(function (done) { + self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () { + done(); + }); + }); + + it('dbUpdateUnset supports custom string _id values', function (done) { + connectAndAuthorize(function (err, socket) { + if (err) return done(err); + + var legacyId = 'legacy-string-id-unset'; + var createdAt = new Date().toISOString(); + + treatmentsCollection().insertOne({ + _id: legacyId, + eventType: 'Note', + created_at: createdAt, + notes: 'remove me' + }, function (insertErr) { + if (insertErr) return done(insertErr); + + socket.emit('dbUpdateUnset', { + collection: 'treatments', + _id: legacyId, + data: { + notes: 1 + } + }, function (updateResult) { + should.exist(updateResult); + updateResult.result.should.equal('success'); + + waitForConditionWithWarning({ + condition: function (cb) { + treatmentsCollection().findOne({ _id: legacyId }, cb); + }, + assertion: function (doc) { + should.exist(doc); + should.not.exist(doc.notes); + }, + done: done, + operationName: 'verify websocket dbUpdateUnset with custom string _id' + }); + }); + }); + }); + }); }); describe('dbRemove operations', function () { @@ -300,6 +395,97 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () { }); }); }); + + it('dbRemove supports custom string _id values', function (done) { + connectAndAuthorize(function (err, socket) { + if (err) return done(err); + + var legacyId = 'legacy-string-id-remove'; + var createdAt = new Date().toISOString(); + + treatmentsCollection().insertOne({ + _id: legacyId, + eventType: 'Note', + created_at: createdAt, + notes: 'delete me' + }, function (insertErr) { + if (insertErr) return done(insertErr); + + socket.emit('dbRemove', { + collection: 'treatments', + _id: legacyId + }, function (removeResult) { + should.exist(removeResult); + removeResult.result.should.equal('success'); + + waitForConditionWithWarning({ + condition: function (cb) { + treatmentsCollection().findOne({ _id: legacyId }, cb); + }, + assertion: function (doc) { + should.not.exist(doc); + }, + done: done, + operationName: 'verify websocket dbRemove with custom string _id' + }); + }); + }); + }); + }); + }); + + describe('dbAdd dedupe operations', function () { + + beforeEach(function (done) { + self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () { + done(); + }); + }); + + it('dbAdd dedupe updates custom string _id records without ObjectId conversion', function (done) { + connectAndAuthorize(function (err, socket) { + if (err) return done(err); + + var legacyId = 'legacy-string-id-dedupe'; + var originalCreatedAt = new Date().toISOString(); + var dedupedCreatedAt = new Date(Date.now() + 1000).toISOString(); + + treatmentsCollection().insertOne({ + _id: legacyId, + eventType: 'Note', + created_at: originalCreatedAt, + notes: 'existing legacy note' + }, function (insertErr) { + if (insertErr) return done(insertErr); + + socket.emit('dbAdd', { + collection: 'treatments', + data: { + eventType: 'Note', + created_at: dedupedCreatedAt, + notes: 'incoming legacy note' + } + }, 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) { + treatmentsCollection().findOne({ _id: legacyId }, cb); + }, + assertion: function (doc) { + should.exist(doc); + doc.created_at.should.equal(dedupedCreatedAt); + }, + done: done, + operationName: 'verify websocket dbAdd dedupe with custom string _id' + }); + }); + }); + }); + }); }); });