mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Add 3 new tests for explicit identifier field handling: - supports identifier field for AAPS-style treatments - deduplicates by identifier on re-upload - supports batch upload with identifiers These complement existing UUID _id tests (Loop pattern) to cover both AID client sync patterns. Refs: REQ-SYNC-072, GAP-TREAT-012 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
520 lines
19 KiB
JavaScript
520 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
var _ = require('lodash');
|
|
var request = require('supertest');
|
|
var should = require('should');
|
|
var language = require('../lib/language')();
|
|
var _moment = require('moment');
|
|
|
|
describe('Treatment API', function ( ) {
|
|
this.timeout(10000);
|
|
var self = this;
|
|
|
|
var api_secret_hash = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
|
|
|
var api = require('../lib/api/');
|
|
|
|
// Use before() instead of beforeEach() for app setup - boots once for all tests
|
|
before(function (done) {
|
|
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'];
|
|
this.wares = require('../lib/middleware/')(self.env);
|
|
self.app = require('express')();
|
|
self.app.enable('api');
|
|
require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) {
|
|
self.ctx = ctx;
|
|
self.ctx.ddata = require('../lib/data/ddata')();
|
|
self.app.use('/api', api(self.env, ctx));
|
|
done();
|
|
});
|
|
});
|
|
|
|
after(function () {
|
|
// delete process.env.API_SECRET;
|
|
});
|
|
|
|
it('post single treatments', function (done) {
|
|
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
|
|
var now = (new Date()).toISOString();
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl', notes: '<IMG SRC="javascript:alert(\'XSS\');">'})
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
self.ctx.treatments.list({}, function (err, list) {
|
|
var sorted = _.sortBy(list, function (treatment) {
|
|
return treatment.created_at;
|
|
});
|
|
sorted.length.should.equal(2);
|
|
sorted[0].glucose.should.equal(100);
|
|
sorted[0].notes.should.equal('<img>');
|
|
should.not.exist(sorted[0].eventTime);
|
|
sorted[0].insulin.should.equal(2);
|
|
sorted[1].carbs.should.equal(30);
|
|
done();
|
|
});
|
|
}
|
|
});
|
|
|
|
});
|
|
});
|
|
|
|
/*
|
|
it('saving entry without created_at should fail', function (done) {
|
|
|
|
self.ctx.treatments().remove({ }, function ( ) {
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', self.env.api_secret || '')
|
|
.send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'})
|
|
.expect(422)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
*/
|
|
|
|
it('post single treatments in zoned time format', function (done) {
|
|
|
|
var current_time = Date.now();
|
|
console.log('Testing date with local format: ', _moment(current_time).format("YYYY-MM-DDTHH:mm:ss.SSSZZ"));
|
|
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({eventType: 'Meal Bolus', created_at: _moment(current_time).format("YYYY-MM-DDTHH:mm:ss.SSSZZ"), carbs: '30', insulin: '2.00', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'})
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
self.ctx.treatments.list({}, function (err, list) {
|
|
var sorted = _.sortBy(list, function (treatment) {
|
|
return treatment.created_at;
|
|
});
|
|
console.log(sorted);
|
|
sorted.length.should.equal(1);
|
|
sorted[0].glucose.should.equal(100);
|
|
should.not.exist(sorted[0].eventTime);
|
|
sorted[0].insulin.should.equal(2);
|
|
sorted[0].carbs.should.equal(30);
|
|
var zonedTime = _moment(current_time).utc().format("YYYY-MM-DDTHH:mm:ss.SSS") + "Z";
|
|
sorted[0].created_at.should.equal(zonedTime);
|
|
sorted[0].utcOffset.should.equal(-1* new Date().getTimezoneOffset());
|
|
done();
|
|
});
|
|
}
|
|
});
|
|
|
|
});
|
|
});
|
|
|
|
|
|
it('post a treatment array', function (done) {
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
|
|
var now = (new Date()).toISOString();
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send([
|
|
{eventType: 'BG Check', created_at: now, glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''}
|
|
, {eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}
|
|
])
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
self.ctx.treatments.list({}, function (err, list) {
|
|
list.length.should.equal(3);
|
|
should.not.exist(list[0].eventTime);
|
|
should.not.exist(list[1].eventTime);
|
|
|
|
done();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
it('post a treatment array and dedupe', function (done) {
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function ( ) {
|
|
var now = (new Date()).toISOString();
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send([
|
|
{eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now}
|
|
, {eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}
|
|
])
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
self.ctx.treatments.list({}, function (err, list) {
|
|
var sorted = _.sortBy(list, function (treatment) {
|
|
return treatment.created_at;
|
|
});
|
|
|
|
if (sorted.length !== 3) {
|
|
console.info('unexpected result length, sorted treatments:', sorted);
|
|
}
|
|
sorted.length.should.equal(3);
|
|
sorted[0].glucose.should.equal(100);
|
|
|
|
done();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
it('post a treatment, query, delete, verify gone', function (done) {
|
|
// insert a treatment - needs to be unique from example data
|
|
console.log('Inserting treatment entry');
|
|
var now = (new Date()).toISOString();
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({eventType: 'Meal Bolus', created_at: now, carbs: '99', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'})
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
// make sure treatment was inserted successfully
|
|
console.log('Ensuring treatment entry was inserted successfully');
|
|
request(self.app)
|
|
.get('/api/treatments/')
|
|
.query('find[carbs]=99')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.expect(200)
|
|
.expect(function (response) {
|
|
response.body[0].carbs.should.equal(99);
|
|
})
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
// delete the treatment
|
|
console.log('Deleting test treatment entry');
|
|
request(self.app)
|
|
.delete('/api/treatments/')
|
|
.query('find[carbs]=99')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
done(err);
|
|
} else {
|
|
// make sure it was deleted
|
|
console.log('Testing if entry was deleted');
|
|
request(self.app)
|
|
.get('/api/treatments/')
|
|
.query('find[carbs]=99')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.expect(200)
|
|
.expect(function (response) {
|
|
response.body.length.should.equal(0);
|
|
})
|
|
.end(done);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
it('supports UUID treatment ids for post, put, and delete', function (done) {
|
|
var treatmentId = '69F15FD2-8075-4DEB-AEA3-4352F455840D';
|
|
// Use recent dates to avoid default query time filter (4 days window)
|
|
var now = new Date();
|
|
var originalCreatedAt = new Date(now.getTime() - 3600000).toISOString(); // 1 hour ago
|
|
var repostedCreatedAt = new Date(now.getTime() - 1800000).toISOString(); // 30 min ago
|
|
var updatedCreatedAt = new Date(now.getTime() - 900000).toISOString(); // 15 min ago
|
|
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({
|
|
_id: treatmentId,
|
|
eventType: 'Temporary Override',
|
|
created_at: originalCreatedAt,
|
|
durationType: 'indefinite',
|
|
correctionRange: [90, 110],
|
|
insulinNeedsScaleFactor: 1.2,
|
|
reason: 'test override'
|
|
})
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
// REQ-SYNC-072: UUID _id is moved to identifier, _id is server-generated ObjectId
|
|
var createdTreatment = res.body[0];
|
|
createdTreatment.identifier.should.equal(treatmentId);
|
|
createdTreatment._id.should.match(/^[0-9a-f]{24}$/); // ObjectId format
|
|
var serverId = createdTreatment._id;
|
|
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({
|
|
_id: treatmentId,
|
|
eventType: 'Temporary Override',
|
|
created_at: repostedCreatedAt,
|
|
duration: 60,
|
|
correctionRange: [90, 110],
|
|
insulinNeedsScaleFactor: 1.2,
|
|
reason: 'reposted override'
|
|
})
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
// REQ-SYNC-072: Lookup by identifier
|
|
self.ctx.treatments.list({ find: { identifier: treatmentId } }, function (err, list) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
list.length.should.equal(1);
|
|
list[0].identifier.should.equal(treatmentId);
|
|
list[0].created_at.should.equal(repostedCreatedAt);
|
|
list[0].duration.should.equal(60);
|
|
should.not.exist(list[0].durationType);
|
|
|
|
request(self.app)
|
|
.put('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({
|
|
_id: treatmentId,
|
|
eventType: 'Temporary Override',
|
|
created_at: updatedCreatedAt,
|
|
duration: 30,
|
|
correctionRange: [90, 110],
|
|
insulinNeedsScaleFactor: 1.2,
|
|
reason: 'updated override'
|
|
})
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
self.ctx.treatments.list({ find: { identifier: treatmentId } }, function (err, updatedList) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
updatedList.length.should.equal(1);
|
|
updatedList[0].identifier.should.equal(treatmentId);
|
|
updatedList[0].created_at.should.equal(updatedCreatedAt);
|
|
updatedList[0].duration.should.equal(30);
|
|
updatedList[0].reason.should.equal('updated override');
|
|
|
|
// Delete by server-assigned _id (or could use identifier)
|
|
request(self.app)
|
|
.delete('/api/treatments/' + encodeURIComponent(serverId))
|
|
.set('api-secret', api_secret_hash || '')
|
|
.expect(200)
|
|
.end(function (err) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
self.ctx.treatments.list({ find: { identifier: treatmentId } }, function (err, deletedList) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
deletedList.length.should.equal(0);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// REQ-SYNC-072: identifier-based CRUD (AAPS pattern)
|
|
// AAPS sends explicit `identifier` field, not UUID as `_id`
|
|
// ============================================================
|
|
|
|
it('supports identifier field for AAPS-style treatments', function (done) {
|
|
var identifier = 'AAPS-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
var now = new Date();
|
|
var createdAt = new Date(now.getTime() - 3600000).toISOString();
|
|
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
|
// POST with explicit identifier (AAPS pattern)
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({
|
|
identifier: identifier,
|
|
eventType: 'Bolus',
|
|
insulin: 2.5,
|
|
created_at: createdAt
|
|
})
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
var createdTreatment = res.body[0];
|
|
createdTreatment.identifier.should.equal(identifier);
|
|
createdTreatment._id.should.match(/^[0-9a-f]{24}$/); // Server-assigned ObjectId
|
|
createdTreatment.insulin.should.equal(2.5);
|
|
|
|
// Query by identifier
|
|
self.ctx.treatments.list({ find: { identifier: identifier } }, function (err, list) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
list.length.should.equal(1);
|
|
list[0].identifier.should.equal(identifier);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('deduplicates by identifier on re-upload', function (done) {
|
|
var identifier = 'DEDUP-' + Date.now();
|
|
var now = new Date();
|
|
var createdAt = new Date(now.getTime() - 3600000).toISOString();
|
|
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
|
// First POST
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({
|
|
identifier: identifier,
|
|
eventType: 'Carb Correction',
|
|
carbs: 30,
|
|
created_at: createdAt
|
|
})
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
var firstId = res.body[0]._id;
|
|
|
|
// Second POST with same identifier - should upsert
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send({
|
|
identifier: identifier,
|
|
eventType: 'Carb Correction',
|
|
carbs: 45, // Updated value
|
|
created_at: createdAt
|
|
})
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
var secondId = res.body[0]._id;
|
|
|
|
// Should be same document (upserted)
|
|
secondId.should.equal(firstId);
|
|
res.body[0].carbs.should.equal(45);
|
|
|
|
// Verify only one document exists
|
|
self.ctx.treatments.list({ find: { identifier: identifier } }, function (err, list) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
list.length.should.equal(1);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('supports batch upload with identifiers', function (done) {
|
|
var identifiers = [
|
|
'BATCH-1-' + Date.now(),
|
|
'BATCH-2-' + Date.now(),
|
|
'BATCH-3-' + Date.now()
|
|
];
|
|
var now = new Date();
|
|
|
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
|
var treatments = identifiers.map(function (id, idx) {
|
|
return {
|
|
identifier: id,
|
|
eventType: 'Bolus',
|
|
insulin: 1.0 + idx * 0.5,
|
|
created_at: new Date(now.getTime() - (3 - idx) * 3600000).toISOString()
|
|
};
|
|
});
|
|
|
|
request(self.app)
|
|
.post('/api/treatments/')
|
|
.set('api-secret', api_secret_hash || '')
|
|
.send(treatments)
|
|
.expect(200)
|
|
.end(function (err, res) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
res.body.length.should.equal(3);
|
|
|
|
// Verify identifiers preserved in order
|
|
res.body[0].identifier.should.equal(identifiers[0]);
|
|
res.body[1].identifier.should.equal(identifiers[1]);
|
|
res.body[2].identifier.should.equal(identifiers[2]);
|
|
|
|
// All should have ObjectId _id
|
|
res.body.forEach(function (t) {
|
|
t._id.should.match(/^[0-9a-f]{24}$/);
|
|
});
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|