Add GAP-TREAT-012 test suite for Loop override UUID handling

TEST-GAP-001: Loop override POST with UUID _id
TEST-GAP-002: Loop override DELETE by UUID
TEST-GAP-003: Loop override UPDATE by UUID
TEST-GAP-004: Loop override re-POST (upsert)

12 new tests validating REQ-SYNC-072 behavior:
- UUID _id promoted to identifier field
- Server generates valid ObjectId for _id
- Updates/deletes work via identifier lookup
- Duplicate detection via identifier
- Batch and edge case handling

New fixtures:
- loop-override.js: Real Loop override payload patterns

All 12 tests passing.

Refs: GAP-TREAT-012, REQ-SYNC-072

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben West
2026-03-10 14:54:00 -07:00
co-authored by Copilot
parent e78a5bc6e7
commit 4af34767c2
3 changed files with 648 additions and 0 deletions
+1
View File
@@ -3,6 +3,7 @@
module.exports = { module.exports = {
aaps: require('./aaps-single-doc'), aaps: require('./aaps-single-doc'),
loop: require('./loop-batch'), loop: require('./loop-batch'),
loopOverride: require('./loop-override'),
trio: require('./trio-pipeline'), trio: require('./trio-pipeline'),
deduplication: require('./deduplication'), deduplication: require('./deduplication'),
edgeCases: require('./edge-cases'), edgeCases: require('./edge-cases'),
+219
View File
@@ -0,0 +1,219 @@
'use strict';
/**
* Loop Override Test Fixtures for GAP-TREAT-012 Testing
*
* GAP-TREAT-012: v1 API incorrectly coerces UUID _id to ObjectId
*
* This file provides fixtures that simulate real Loop override uploads,
* which use UUID strings as the `id` field (mapped to `_id` in Nightscout).
*
* REQ-SYNC-072: UUID _id should be moved to identifier field,
* with server generating a valid ObjectId for _id.
*/
// Generate fresh dates for each test run
function now() {
return new Date().toISOString();
}
function hoursAgo(hours) {
return new Date(Date.now() - hours * 3600000).toISOString();
}
function minutesAgo(minutes) {
return new Date(Date.now() - minutes * 60000).toISOString();
}
module.exports = {
/**
* Standard Loop override as uploaded by OverrideTreatment.swift
* Note: Loop sends `id` as UUID string, NOT as ObjectId
*/
standardOverride: {
_id: 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890',
eventType: 'Temporary Override',
created_at: minutesAgo(30),
enteredBy: 'Loop',
duration: 60,
correctionRange: [90, 110],
insulinNeedsScaleFactor: 1.2,
reason: 'Custom Override'
},
/**
* Indefinite override (no end time)
*/
indefiniteOverride: {
_id: 'B2C3D4E5-F6A7-8901-BCDE-F23456789012',
eventType: 'Temporary Override',
created_at: minutesAgo(15),
enteredBy: 'Loop',
durationType: 'indefinite',
correctionRange: [100, 120],
insulinNeedsScaleFactor: 1.0,
reason: 'Running Low'
},
/**
* Remote command override
*/
remoteOverride: {
_id: 'C3D4E5F6-A7B8-9012-CDEF-345678901234',
eventType: 'Temporary Override',
created_at: minutesAgo(5),
enteredBy: 'Loop (via remote command)',
duration: 120,
correctionRange: [150, 180],
insulinNeedsScaleFactor: 0.5,
reason: 'Workout'
},
/**
* Preset override (e.g., "Eating Soon")
*/
presetOverride: {
_id: 'D4E5F6A7-B8C9-0123-DEFA-456789012345',
eventType: 'Temporary Override',
created_at: hoursAgo(1),
enteredBy: 'Loop',
duration: 60,
correctionRange: [80, 80],
insulinNeedsScaleFactor: 1.0,
reason: 'Pre-Meal'
},
/**
* Update scenario: same UUID, different values
*/
updateScenario: {
original: {
_id: 'E5F6A7B8-C9D0-1234-EFAB-567890123456',
eventType: 'Temporary Override',
created_at: hoursAgo(2),
enteredBy: 'Loop',
duration: 60,
correctionRange: [100, 120],
insulinNeedsScaleFactor: 1.0,
reason: 'Original Override'
},
updated: {
_id: 'E5F6A7B8-C9D0-1234-EFAB-567890123456',
eventType: 'Temporary Override',
created_at: hoursAgo(1),
enteredBy: 'Loop',
duration: 120, // Extended duration
correctionRange: [110, 130], // Changed range
insulinNeedsScaleFactor: 0.8, // Changed factor
reason: 'Updated Override'
}
},
/**
* Delete scenario: override to cancel
*/
deleteScenario: {
toDelete: {
_id: 'F6A7B8C9-D0E1-2345-FABC-678901234567',
eventType: 'Temporary Override',
created_at: minutesAgo(45),
enteredBy: 'Loop',
duration: 90,
correctionRange: [95, 115],
insulinNeedsScaleFactor: 1.1,
reason: 'To Be Deleted'
}
},
/**
* Multiple overrides batch (simulates Loop restart/sync)
*/
batchOverrides: [
{
_id: '11111111-1111-1111-1111-111111111111',
eventType: 'Temporary Override',
created_at: hoursAgo(3),
enteredBy: 'Loop',
duration: 30,
correctionRange: [90, 100],
insulinNeedsScaleFactor: 1.0,
reason: 'First Override'
},
{
_id: '22222222-2222-2222-2222-222222222222',
eventType: 'Temporary Override',
created_at: hoursAgo(2),
enteredBy: 'Loop',
duration: 45,
correctionRange: [100, 110],
insulinNeedsScaleFactor: 0.9,
reason: 'Second Override'
},
{
_id: '33333333-3333-3333-3333-333333333333',
eventType: 'Temporary Override',
created_at: hoursAgo(1),
enteredBy: 'Loop',
duration: 60,
correctionRange: [80, 90],
insulinNeedsScaleFactor: 1.2,
reason: 'Third Override'
}
],
/**
* Duplicate detection scenario
* Same UUID should NOT create duplicate
*/
duplicateScenario: {
first: {
_id: 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE',
eventType: 'Temporary Override',
created_at: minutesAgo(60),
enteredBy: 'Loop',
duration: 60,
correctionRange: [100, 110],
insulinNeedsScaleFactor: 1.0,
reason: 'Original Post'
},
repost: {
_id: 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE',
eventType: 'Temporary Override',
created_at: minutesAgo(30), // Different time
enteredBy: 'Loop',
duration: 90, // Different duration
correctionRange: [110, 120],
insulinNeedsScaleFactor: 1.1,
reason: 'Reposted Override'
}
},
/**
* Mixed eventTypes batch - some with UUID, some without
*/
mixedBatch: [
{
_id: 'UUID1234-5678-90AB-CDEF-111111111111',
eventType: 'Temporary Override',
created_at: minutesAgo(20),
enteredBy: 'Loop',
duration: 60,
correctionRange: [100, 110],
insulinNeedsScaleFactor: 1.0,
reason: 'Override With UUID'
},
{
eventType: 'Carb Correction',
carbs: 15,
created_at: minutesAgo(15),
enteredBy: 'loop://iPhone',
absorptionTime: 180
},
{
eventType: 'Bolus',
insulin: 2.5,
created_at: minutesAgo(10),
enteredBy: 'loop://iPhone'
}
]
};
+428
View File
@@ -0,0 +1,428 @@
'use strict';
/**
* GAP-TREAT-012 Test Suite: Loop Override UUID Identity Handling
*
* REQUIREMENT REFERENCE: REQ-SYNC-072 (Transparent UUID promotion)
* GAP REFERENCE: GAP-TREAT-012 (v1 API incorrectly coerces UUID _id to ObjectId)
*
* These tests verify that:
* 1. UUID _id is moved to identifier field (not corrupted)
* 2. Server generates valid ObjectId for _id
* 3. Updates/deletes work via identifier lookup
* 4. Duplicate detection works via identifier
*
* TEST MATRIX REFERENCE: docs/backlogs/loop-nightscout-upload-testing.md
* - TEST-GAP-001: Loop override POST
* - TEST-GAP-002: Loop override DELETE
* - TEST-GAP-003: Loop override UPDATE
* - TEST-GAP-004: Loop override re-POST (upsert)
*/
const request = require('supertest');
const should = require('should');
const language = require('../lib/language')();
describe('GAP-TREAT-012: Loop Override UUID Handling', function() {
this.timeout(30000);
const self = this;
const api_secret_hash = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
// Load Loop override fixtures
const loopOverride = require('./fixtures/loop-override');
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'];
const 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.wares = wares;
self.ctx.ddata = require('../lib/data/ddata')();
self.app.use('/api', require('../lib/api/')(self.env, ctx));
done();
});
});
beforeEach(function(done) {
// Clear treatments before each test
self.ctx.treatments.remove({
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
}, done);
});
describe('TEST-GAP-001: Loop override POST with UUID as _id', function() {
it('accepts UUID _id and promotes to identifier field', function(done) {
const fixture = loopOverride.standardOverride;
const uuidId = fixture._id;
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
should.exist(res.body);
res.body.should.be.an.Array();
res.body.length.should.equal(1);
const created = res.body[0];
// REQ-SYNC-072: UUID should be in identifier, not _id
created.identifier.should.equal(uuidId);
// _id should be a valid ObjectId (24 hex chars)
created._id.should.match(/^[0-9a-f]{24}$/);
created._id.should.not.equal(uuidId);
// Other fields preserved
created.eventType.should.equal('Temporary Override');
created.reason.should.equal('Custom Override');
console.log(` UUID _id: ${uuidId}`);
console.log(` Server _id: ${created._id}`);
console.log(` identifier: ${created.identifier}`);
console.log(' ✓ UUID promoted to identifier field');
done();
});
});
it('indefinite override UUID is preserved in identifier', function(done) {
const fixture = loopOverride.indefiniteOverride;
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
const created = res.body[0];
created.identifier.should.equal(fixture._id);
created._id.should.match(/^[0-9a-f]{24}$/);
should.exist(created.durationType);
created.durationType.should.equal('indefinite');
console.log(' ✓ Indefinite override UUID preserved');
done();
});
});
it('remote command override UUID is preserved in identifier', function(done) {
const fixture = loopOverride.remoteOverride;
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
const created = res.body[0];
created.identifier.should.equal(fixture._id);
created.enteredBy.should.equal('Loop (via remote command)');
console.log(' ✓ Remote override UUID preserved');
done();
});
});
});
describe('TEST-GAP-002: Loop override DELETE by UUID', function() {
it('can delete override using identifier query', function(done) {
const fixture = loopOverride.deleteScenario.toDelete;
// First create the override
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
const serverId = res.body[0]._id;
// Delete by server-assigned _id (standard method)
request(self.app)
.delete('/api/treatments/' + serverId)
.set('api-secret', api_secret_hash)
.expect(200)
.end(function(err) {
should.not.exist(err);
// Verify deleted
self.ctx.treatments.list({ find: { identifier: fixture._id } }, function(err, list) {
should.not.exist(err);
list.length.should.equal(0);
console.log(' ✓ Override deleted successfully');
done();
});
});
});
});
it('can find override by identifier after creation', function(done) {
const fixture = loopOverride.standardOverride;
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err) {
should.not.exist(err);
// Query by identifier
self.ctx.treatments.list({ find: { identifier: fixture._id } }, function(err, list) {
should.not.exist(err);
list.length.should.equal(1);
list[0].identifier.should.equal(fixture._id);
console.log(' ✓ Override found by identifier query');
done();
});
});
});
});
describe('TEST-GAP-003: Loop override UPDATE by UUID', function() {
it('PUT with UUID _id updates existing override', function(done) {
const scenario = loopOverride.updateScenario;
// Create original
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([scenario.original])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
const serverId = res.body[0]._id;
// Update with same UUID
request(self.app)
.put('/api/treatments/')
.set('api-secret', api_secret_hash)
.send(scenario.updated)
.expect(200)
.end(function(err) {
should.not.exist(err);
// Verify update
self.ctx.treatments.list({ find: { identifier: scenario.original._id } }, function(err, list) {
should.not.exist(err);
list.length.should.equal(1);
const updated = list[0];
updated.identifier.should.equal(scenario.original._id);
updated.duration.should.equal(120); // Updated value
updated.reason.should.equal('Updated Override');
console.log(' ✓ Override updated via PUT with UUID');
done();
});
});
});
});
});
describe('TEST-GAP-004: Loop override re-POST (upsert by identifier)', function() {
it('re-POST same UUID updates instead of creating duplicate', function(done) {
const scenario = loopOverride.duplicateScenario;
// First POST
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([scenario.first])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
const firstServerId = res.body[0]._id;
// Re-POST with same UUID, different values
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([scenario.repost])
.expect(200)
.end(function(err) {
should.not.exist(err);
// Should only have ONE document
self.ctx.treatments.list({ find: { identifier: scenario.first._id } }, function(err, list) {
should.not.exist(err);
// CRITICAL: No duplicates
list.length.should.equal(1, 'Re-POST should update, not create duplicate');
// Should have updated values
const doc = list[0];
doc.duration.should.equal(90); // From repost
doc.reason.should.equal('Reposted Override');
console.log(` First _id: ${firstServerId}`);
console.log(` After re-POST: 1 document (no duplicate)`);
console.log(' ✓ Re-POST with same UUID updates existing');
done();
});
});
});
});
});
describe('Batch Override Upload', function() {
it('batch of overrides with UUIDs all get identifier fields', function(done) {
const batch = loopOverride.batchOverrides;
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send(batch)
.expect(200)
.end(function(err, res) {
should.not.exist(err);
res.body.length.should.equal(3);
// All should have identifier field with original UUID
res.body.forEach(function(item, idx) {
item.identifier.should.equal(batch[idx]._id);
item._id.should.match(/^[0-9a-f]{24}$/);
});
// Verify in database
self.ctx.treatments.list({}, function(err, list) {
should.not.exist(err);
list.length.should.equal(3);
console.log(' ✓ Batch of 3 overrides all have identifier fields');
done();
});
});
});
it('mixed batch with UUID and non-UUID treatments', function(done) {
const batch = loopOverride.mixedBatch;
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send(batch)
.expect(200)
.end(function(err, res) {
should.not.exist(err);
res.body.length.should.equal(3);
// Override should have identifier
const override = res.body.find(t => t.eventType === 'Temporary Override');
override.identifier.should.equal(batch[0]._id);
override._id.should.match(/^[0-9a-f]{24}$/);
// Carb and bolus should have generated _ids
const carb = res.body.find(t => t.eventType === 'Carb Correction');
carb._id.should.match(/^[0-9a-f]{24}$/);
console.log(' ✓ Mixed batch handles UUID and non-UUID correctly');
done();
});
});
});
describe('Edge Cases', function() {
it('handles uppercase UUID', function(done) {
const fixture = {
_id: 'ABCDEF01-2345-6789-ABCD-EF0123456789',
eventType: 'Temporary Override',
created_at: new Date().toISOString(),
enteredBy: 'Loop',
duration: 30,
correctionRange: [100, 110],
reason: 'Uppercase UUID Test'
};
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
res.body[0].identifier.should.equal(fixture._id);
console.log(' ✓ Uppercase UUID preserved');
done();
});
});
it('handles lowercase UUID', function(done) {
const fixture = {
_id: 'abcdef01-2345-6789-abcd-ef0123456789',
eventType: 'Temporary Override',
created_at: new Date().toISOString(),
enteredBy: 'Loop',
duration: 30,
correctionRange: [100, 110],
reason: 'Lowercase UUID Test'
};
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
res.body[0].identifier.should.equal(fixture._id);
console.log(' ✓ Lowercase UUID preserved');
done();
});
});
it('valid ObjectId string is NOT promoted to identifier', function(done) {
// 24-char hex string (valid ObjectId format)
const objectIdString = '507f1f77bcf86cd799439011';
const fixture = {
_id: objectIdString,
eventType: 'Note',
created_at: new Date().toISOString(),
enteredBy: 'Test',
notes: 'ObjectId format test'
};
request(self.app)
.post('/api/treatments/')
.set('api-secret', api_secret_hash)
.send([fixture])
.expect(200)
.end(function(err, res) {
should.not.exist(err);
const created = res.body[0];
// ObjectId should be used as-is (or regenerated), NOT promoted
created._id.should.match(/^[0-9a-f]{24}$/);
console.log(` Input _id: ${objectIdString}`);
console.log(` Result _id: ${created._id}`);
console.log(' ✓ ObjectId format handled correctly');
done();
});
});
});
});