mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Add comprehensive tests for real-world AndroidAPS data handling scenarios
Introduce new test suite `tests/api3.aaps-patterns.test.js` and update `tests/concurrent-writes.test.js` to simulate AAPS sync catch-up with rapid sequential SMB and SGV POST requests, ensuring correct deduplication and data persistence for various AndroidAPS usage patterns. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 2501b04e-77c8-4f06-a13c-75c232bafd47 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 804cc747-c899-430f-9181-96a22d4301a1 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ea4278b-5c6c-4065-9cb8-f1013771318d/2501b04e-77c8-4f06-a13c-75c232bafd47/Bc7Z6xs Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -244,9 +244,54 @@ The MongoDB 5.x driver has stricter typing. Key behaviors:
|
||||
| Cross-collection concurrent writes | Medium | ✅ Covered |
|
||||
| Unique _id after concurrent inserts | High | ✅ Covered |
|
||||
| Response count matches request count | High | ✅ Covered |
|
||||
| AAPS sync catch-up (50 SMB POSTs) | High | ✅ Covered |
|
||||
| AAPS sync catch-up (100 SGV POSTs) | High | ✅ Covered |
|
||||
| Cross-collection concurrent sync | Medium | ✅ Covered |
|
||||
| WebSocket + API concurrent writes | Medium | ⬜ Not covered |
|
||||
| Duplicate identifier handling under load | Medium | ⬜ Not covered |
|
||||
|
||||
---
|
||||
|
||||
## AAPS-Realistic Pattern Tests
|
||||
|
||||
Based on analysis of AndroidAPS source code. See `tests/fixtures/aaps-patterns.json` for fixture data.
|
||||
|
||||
### API v3 Deduplication Tests
|
||||
|
||||
The identifier is calculated from `device + date + eventType`. Note: `pumpId`, `pumpType`, `pumpSerial` are stored but NOT used in identifier calculation.
|
||||
|
||||
| Test Case | Status |
|
||||
|-----------|--------|
|
||||
| New treatment returns 201 | ✅ Covered |
|
||||
| Duplicate (same device+date+eventType) returns 200 | ✅ Covered |
|
||||
| Different pumpId, same identifier triggers dedup | ✅ Covered |
|
||||
| Different date creates new treatment (201) | ✅ Covered |
|
||||
| Different eventType creates new treatment (201) | ✅ Covered |
|
||||
|
||||
### SMB Burst Pattern Tests
|
||||
|
||||
Tests rapid sequential Super Micro Bolus insertions (common in closed-loop systems).
|
||||
|
||||
| Test Case | Status |
|
||||
|-----------|--------|
|
||||
| Sequential SMB corrections with unique identifiers | ✅ Covered |
|
||||
|
||||
### Meal Scenario Tests
|
||||
|
||||
Tests typical meal-related treatments: carb entry, bolus wizard, meal bolus.
|
||||
|
||||
| Test Case | Status |
|
||||
|-----------|--------|
|
||||
| All meal treatments have unique identifiers | ✅ Covered |
|
||||
|
||||
### SGV Entry Pattern Tests
|
||||
|
||||
Tests high-frequency glucose readings (5-minute intervals).
|
||||
|
||||
| Test Case | Status |
|
||||
|-----------|--------|
|
||||
| Sequential SGV entries with unique identifiers | ✅ Covered |
|
||||
|
||||
### Expected Behavior
|
||||
- All writes should succeed or fail atomically
|
||||
- No data corruption or partial writes
|
||||
|
||||
@@ -76,9 +76,11 @@ Comprehensive test coverage for single document vs array input handling across a
|
||||
|------|---------------|
|
||||
| `tests/api.shape-handling.test.js` | API v1 treatments, devicestatus, entries - single/array input, response shapes |
|
||||
| `tests/api3.shape-handling.test.js` | API v3 all collections - single object only (arrays rejected) |
|
||||
| `tests/api3.aaps-patterns.test.js` | API v3 AAPS-realistic patterns - deduplication, SMB bursts, meal scenarios |
|
||||
| `tests/storage.shape-handling.test.js` | Storage layer direct tests, MongoDB insertOne vs insertMany |
|
||||
| `tests/websocket.shape-handling.test.js` | WebSocket dbAdd/dbUpdate/dbRemove operations |
|
||||
| `tests/concurrent-writes.test.js` | Race conditions, concurrent access, MongoDB 5.x compatibility |
|
||||
| `tests/concurrent-writes.test.js` | Race conditions, concurrent access, MongoDB 5.x, AAPS sync catch-up |
|
||||
| `tests/fixtures/aaps-patterns.json` | AAPS-realistic test fixtures (SGV, SMB, meals, deduplication) |
|
||||
|
||||
### API Behavior Summary
|
||||
| Interface | Single Object | Array Input | Response Format |
|
||||
@@ -89,11 +91,19 @@ Comprehensive test coverage for single document vs array input handling across a
|
||||
| API v3 `/api/v3/{collection}` | Supported | Rejected (400) | Single Object |
|
||||
| WebSocket `dbAdd` | Supported | Supported | Always Array |
|
||||
|
||||
### API v3 Deduplication Logic
|
||||
The server calculates identifiers from `device + date + eventType`. Important findings:
|
||||
- `pumpId`, `pumpType`, `pumpSerial` are stored but NOT used in identifier calculation
|
||||
- AAPS sends documents one-at-a-time (not in batches) to API v3
|
||||
- "Multiple document handling" concerns are about rapid sequential requests during sync catch-up
|
||||
- Fallback dedup fields per collection: treatments=`[created_at, eventType]`, entries=`[date, type]`
|
||||
|
||||
### Running Shape Handling Tests
|
||||
```bash
|
||||
npm test -- --grep "Shape Handling"
|
||||
npm test -- tests/api.shape-handling.test.js
|
||||
npm test -- tests/concurrent-writes.test.js
|
||||
npm test -- tests/api3.aaps-patterns.test.js
|
||||
```
|
||||
|
||||
## External Dependencies
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
'use strict';
|
||||
|
||||
require('should');
|
||||
|
||||
describe('API3 AAPS Patterns - Deduplication and Real-world Scenarios', function() {
|
||||
const self = this;
|
||||
const testConst = require('./fixtures/api3/const.json');
|
||||
const aapsPatterns = require('./fixtures/aaps-patterns.json');
|
||||
const instance = require('./fixtures/api3/instance');
|
||||
const authSubject = require('./fixtures/api3/authSubject');
|
||||
const opTools = require('../lib/api3/shared/operationTools');
|
||||
|
||||
self.timeout(30000);
|
||||
|
||||
before(async () => {
|
||||
self.instance = await instance.create({});
|
||||
self.app = self.instance.app;
|
||||
self.env = self.instance.env;
|
||||
|
||||
let authResult = await authSubject(self.instance.ctx.authorization.storage, [
|
||||
'create',
|
||||
'update',
|
||||
'read',
|
||||
'delete',
|
||||
'all'
|
||||
], self.instance.app);
|
||||
|
||||
self.subject = authResult.subject;
|
||||
self.jwt = authResult.jwt;
|
||||
self.cache = self.instance.cacheMonitor;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
self.instance.ctx.bus.teardown();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
self.cache.clear();
|
||||
});
|
||||
|
||||
describe('Deduplication Tests - identifier-based (device + date + eventType)', function() {
|
||||
const col = 'treatments';
|
||||
const url = `/api/v3/${col}`;
|
||||
|
||||
async function deleteDoc(identifier) {
|
||||
self.cache.clear();
|
||||
await self.instance.delete(`${url}/${identifier}?permanent=true`, self.jwt.delete)
|
||||
.expect(200);
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
async function getDoc(identifier) {
|
||||
let res = await self.instance.get(`${url}/${identifier}`, self.jwt.read)
|
||||
.expect(200);
|
||||
return res.body.result;
|
||||
}
|
||||
|
||||
it('returns 201 for new treatment with unique identifier and persists AAPS fields', async () => {
|
||||
const baseFixture = aapsPatterns.DEDUPLICATION_TESTS.ORIGINAL;
|
||||
const doc = {
|
||||
device: 'test-device-unique-' + Date.now(),
|
||||
date: Date.now(),
|
||||
app: testConst.TEST_APP,
|
||||
eventType: baseFixture.eventType,
|
||||
insulin: baseFixture.insulin,
|
||||
pumpId: baseFixture.pumpId,
|
||||
pumpType: baseFixture.pumpType,
|
||||
pumpSerial: baseFixture.pumpSerial,
|
||||
isValid: baseFixture.isValid
|
||||
};
|
||||
doc.identifier = opTools.calculateIdentifier(doc);
|
||||
|
||||
let res = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc)
|
||||
.expect(201);
|
||||
|
||||
res.body.status.should.equal(201);
|
||||
res.body.identifier.should.equal(doc.identifier);
|
||||
|
||||
const persisted = await getDoc(doc.identifier);
|
||||
persisted.pumpId.should.equal(baseFixture.pumpId);
|
||||
persisted.pumpType.should.equal(baseFixture.pumpType);
|
||||
persisted.pumpSerial.should.equal(baseFixture.pumpSerial);
|
||||
persisted.insulin.should.equal(baseFixture.insulin);
|
||||
|
||||
await deleteDoc(doc.identifier);
|
||||
});
|
||||
|
||||
it('returns 200 for duplicate with same device+date+eventType (deduplication)', async () => {
|
||||
const doc = {
|
||||
device: 'test-device-dedup-' + Date.now(),
|
||||
date: Date.now(),
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5,
|
||||
isValid: true
|
||||
};
|
||||
doc.identifier = opTools.calculateIdentifier(doc);
|
||||
|
||||
let res1 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc)
|
||||
.expect(201);
|
||||
|
||||
res1.body.status.should.equal(201);
|
||||
self.cache.clear();
|
||||
|
||||
let res2 = await self.instance.post(url, self.jwt.update)
|
||||
.send(doc)
|
||||
.expect(200);
|
||||
|
||||
res2.body.status.should.equal(200);
|
||||
res2.body.identifier.should.equal(doc.identifier);
|
||||
|
||||
await deleteDoc(doc.identifier);
|
||||
});
|
||||
|
||||
it('different pumpId but same device+date+eventType triggers dedup (pumpId not in identifier)', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-pumpid-' + baseTime;
|
||||
|
||||
const doc1 = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5,
|
||||
pumpId: 9998,
|
||||
pumpType: 'DANA_R',
|
||||
pumpSerial: '12345'
|
||||
};
|
||||
doc1.identifier = opTools.calculateIdentifier(doc1);
|
||||
|
||||
const doc2 = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5,
|
||||
pumpId: 9999,
|
||||
pumpType: 'DANA_R',
|
||||
pumpSerial: '12345'
|
||||
};
|
||||
doc2.identifier = opTools.calculateIdentifier(doc2);
|
||||
|
||||
let res1 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc1)
|
||||
.expect(201);
|
||||
|
||||
res1.body.status.should.equal(201);
|
||||
self.cache.clear();
|
||||
|
||||
let res2 = await self.instance.post(url, self.jwt.update)
|
||||
.send(doc2)
|
||||
.expect(200);
|
||||
|
||||
res2.body.status.should.equal(200);
|
||||
res2.body.identifier.should.equal(doc1.identifier);
|
||||
|
||||
await deleteDoc(doc1.identifier);
|
||||
});
|
||||
|
||||
it('different date creates new unique treatment (201)', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-date-' + baseTime;
|
||||
|
||||
const doc1 = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5
|
||||
};
|
||||
doc1.identifier = opTools.calculateIdentifier(doc1);
|
||||
|
||||
const doc2 = {
|
||||
device: device,
|
||||
date: baseTime + 300000,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5
|
||||
};
|
||||
doc2.identifier = opTools.calculateIdentifier(doc2);
|
||||
|
||||
let res1 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc1)
|
||||
.expect(201);
|
||||
|
||||
res1.body.status.should.equal(201);
|
||||
self.cache.clear();
|
||||
|
||||
let res2 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc2)
|
||||
.expect(201);
|
||||
|
||||
res2.body.status.should.equal(201);
|
||||
res2.body.identifier.should.not.equal(res1.body.identifier);
|
||||
|
||||
await deleteDoc(doc1.identifier);
|
||||
await deleteDoc(doc2.identifier);
|
||||
});
|
||||
|
||||
it('different eventType creates new unique treatment (201)', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-event-' + baseTime;
|
||||
|
||||
const doc1 = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5
|
||||
};
|
||||
doc1.identifier = opTools.calculateIdentifier(doc1);
|
||||
|
||||
const doc2 = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Meal Bolus',
|
||||
insulin: 0.5
|
||||
};
|
||||
doc2.identifier = opTools.calculateIdentifier(doc2);
|
||||
|
||||
let res1 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc1)
|
||||
.expect(201);
|
||||
|
||||
res1.body.status.should.equal(201);
|
||||
self.cache.clear();
|
||||
|
||||
let res2 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc2)
|
||||
.expect(201);
|
||||
|
||||
res2.body.status.should.equal(201);
|
||||
res2.body.identifier.should.not.equal(res1.body.identifier);
|
||||
|
||||
await deleteDoc(doc1.identifier);
|
||||
await deleteDoc(doc2.identifier);
|
||||
});
|
||||
|
||||
it('rapid duplicate submissions result in single persisted document with latest srvModified', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-rapid-dedup-' + baseTime;
|
||||
|
||||
const doc = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.5,
|
||||
pumpId: 8888,
|
||||
pumpType: aapsPatterns.PUMP_IDENTIFIERS.DANA_R.pumpType,
|
||||
pumpSerial: aapsPatterns.PUMP_IDENTIFIERS.DANA_R.pumpSerial
|
||||
};
|
||||
doc.identifier = opTools.calculateIdentifier(doc);
|
||||
|
||||
let res1 = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc)
|
||||
.expect(201);
|
||||
res1.body.status.should.equal(201);
|
||||
const firstSrvModified = res1.body.lastModified;
|
||||
self.cache.clear();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
let res2 = await self.instance.post(url, self.jwt.update)
|
||||
.send(doc)
|
||||
.expect(200);
|
||||
res2.body.status.should.equal(200);
|
||||
res2.body.identifier.should.equal(doc.identifier);
|
||||
const secondSrvModified = res2.body.lastModified;
|
||||
secondSrvModified.should.be.greaterThanOrEqual(firstSrvModified);
|
||||
self.cache.clear();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
let res3 = await self.instance.post(url, self.jwt.update)
|
||||
.send(doc)
|
||||
.expect(200);
|
||||
res3.body.status.should.equal(200);
|
||||
res3.body.identifier.should.equal(doc.identifier);
|
||||
const thirdSrvModified = res3.body.lastModified;
|
||||
thirdSrvModified.should.be.greaterThanOrEqual(secondSrvModified);
|
||||
self.cache.clear();
|
||||
|
||||
const persisted = await getDoc(doc.identifier);
|
||||
persisted.identifier.should.equal(doc.identifier);
|
||||
persisted.pumpId.should.equal(8888);
|
||||
persisted.srvModified.should.be.greaterThanOrEqual(persisted.srvCreated);
|
||||
|
||||
const lastResponseTime = new Date(thirdSrvModified).getTime();
|
||||
const persistedModTime = persisted.srvModified;
|
||||
persistedModTime.should.be.approximately(lastResponseTime, 1000);
|
||||
|
||||
let searchRes = await self.instance.get(`${url}?identifier=${doc.identifier}`, self.jwt.read)
|
||||
.expect(200);
|
||||
searchRes.body.result.length.should.equal(1);
|
||||
|
||||
let broadSearch = await self.instance.get(`${url}?device=${device}&date$eq=${baseTime}`, self.jwt.read)
|
||||
.expect(200);
|
||||
broadSearch.body.result.length.should.equal(1);
|
||||
|
||||
await deleteDoc(doc.identifier);
|
||||
});
|
||||
|
||||
it('full AAPS fixture payload with created_at and dateString persists correctly', async () => {
|
||||
const baseTime = Date.now();
|
||||
const baseFixture = aapsPatterns.DEDUPLICATION_TESTS.ORIGINAL;
|
||||
|
||||
const doc = {
|
||||
device: 'test-device-full-fixture-' + baseTime,
|
||||
date: baseTime,
|
||||
created_at: new Date(baseTime).toISOString(),
|
||||
dateString: new Date(baseTime).toISOString(),
|
||||
app: testConst.TEST_APP,
|
||||
eventType: baseFixture.eventType,
|
||||
insulin: baseFixture.insulin,
|
||||
pumpId: baseFixture.pumpId,
|
||||
pumpType: baseFixture.pumpType,
|
||||
pumpSerial: baseFixture.pumpSerial,
|
||||
isValid: baseFixture.isValid,
|
||||
isSMB: false,
|
||||
type: 'NORMAL'
|
||||
};
|
||||
doc.identifier = opTools.calculateIdentifier(doc);
|
||||
|
||||
let res = await self.instance.post(url, self.jwt.create)
|
||||
.send(doc)
|
||||
.expect(201);
|
||||
|
||||
res.body.status.should.equal(201);
|
||||
self.cache.clear();
|
||||
|
||||
const persisted = await getDoc(doc.identifier);
|
||||
persisted.identifier.should.equal(doc.identifier);
|
||||
persisted.pumpId.should.equal(baseFixture.pumpId);
|
||||
persisted.pumpType.should.equal(baseFixture.pumpType);
|
||||
persisted.pumpSerial.should.equal(baseFixture.pumpSerial);
|
||||
persisted.insulin.should.equal(baseFixture.insulin);
|
||||
persisted.eventType.should.equal(baseFixture.eventType);
|
||||
persisted.should.have.property('srvCreated');
|
||||
persisted.should.have.property('srvModified');
|
||||
|
||||
await deleteDoc(doc.identifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SMB Burst Pattern - rapid sequential micro-boluses', function() {
|
||||
const col = 'treatments';
|
||||
const url = `/api/v3/${col}`;
|
||||
|
||||
async function deleteDoc(identifier) {
|
||||
self.cache.clear();
|
||||
await self.instance.delete(`${url}/${identifier}?permanent=true`, self.jwt.delete)
|
||||
.expect(200);
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
async function getDoc(identifier) {
|
||||
let res = await self.instance.get(`${url}/${identifier}`, self.jwt.read)
|
||||
.expect(200);
|
||||
return res.body.result;
|
||||
}
|
||||
|
||||
it('handles sequential SMB corrections with full AAPS-realistic payloads', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-smb-' + baseTime;
|
||||
const identifiers = [];
|
||||
const smbFixtures = aapsPatterns.SMB_BURSTS.CORRECTION_SEQUENCE;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const fixtureSmb = smbFixtures[i];
|
||||
const smb = {
|
||||
device: device,
|
||||
date: baseTime + (i * 300000),
|
||||
created_at: new Date(baseTime + (i * 300000)).toISOString(),
|
||||
app: testConst.TEST_APP,
|
||||
eventType: fixtureSmb.eventType,
|
||||
insulin: fixtureSmb.insulin,
|
||||
isSMB: fixtureSmb.isSMB,
|
||||
pumpId: fixtureSmb.pumpId,
|
||||
pumpType: fixtureSmb.pumpType,
|
||||
pumpSerial: fixtureSmb.pumpSerial,
|
||||
type: fixtureSmb.type || 'NORMAL',
|
||||
isValid: fixtureSmb.isValid
|
||||
};
|
||||
smb.identifier = opTools.calculateIdentifier(smb);
|
||||
|
||||
let res = await self.instance.post(url, self.jwt.create)
|
||||
.send(smb)
|
||||
.expect(201);
|
||||
|
||||
res.body.status.should.equal(201);
|
||||
identifiers.push(res.body.identifier);
|
||||
self.cache.clear();
|
||||
|
||||
const persisted = await getDoc(res.body.identifier);
|
||||
persisted.pumpId.should.equal(fixtureSmb.pumpId);
|
||||
persisted.isSMB.should.equal(true);
|
||||
persisted.insulin.should.equal(fixtureSmb.insulin);
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
const uniqueIds = [...new Set(identifiers)];
|
||||
uniqueIds.length.should.equal(3);
|
||||
|
||||
for (const id of identifiers) {
|
||||
await deleteDoc(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meal Scenario - carbs, bolus wizard, meal bolus', function() {
|
||||
const col = 'treatments';
|
||||
const url = `/api/v3/${col}`;
|
||||
|
||||
async function deleteDoc(identifier) {
|
||||
self.cache.clear();
|
||||
await self.instance.delete(`${url}/${identifier}?permanent=true`, self.jwt.delete)
|
||||
.expect(200);
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
it('creates all meal-related treatments with unique identifiers', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-meal-' + baseTime;
|
||||
const identifiers = [];
|
||||
|
||||
const carbs = {
|
||||
device: device,
|
||||
date: baseTime,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Carb Correction',
|
||||
carbs: 45,
|
||||
isValid: true
|
||||
};
|
||||
carbs.identifier = opTools.calculateIdentifier(carbs);
|
||||
|
||||
let res1 = await self.instance.post(url, self.jwt.create)
|
||||
.send(carbs)
|
||||
.expect(201);
|
||||
identifiers.push(res1.body.identifier);
|
||||
self.cache.clear();
|
||||
|
||||
const wizard = {
|
||||
device: device,
|
||||
date: baseTime + 1000,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Bolus Wizard',
|
||||
glucose: 134,
|
||||
glucoseType: 'Sensor',
|
||||
carbs: 45,
|
||||
insulin: 8.1
|
||||
};
|
||||
wizard.identifier = opTools.calculateIdentifier(wizard);
|
||||
|
||||
let res2 = await self.instance.post(url, self.jwt.create)
|
||||
.send(wizard)
|
||||
.expect(201);
|
||||
identifiers.push(res2.body.identifier);
|
||||
self.cache.clear();
|
||||
|
||||
const bolus = {
|
||||
device: device,
|
||||
date: baseTime + 2000,
|
||||
app: testConst.TEST_APP,
|
||||
eventType: 'Meal Bolus',
|
||||
insulin: 8.1,
|
||||
pumpId: 4102,
|
||||
pumpType: aapsPatterns.PUMP_IDENTIFIERS.ACCU_CHEK_INSIGHT.pumpType,
|
||||
pumpSerial: aapsPatterns.PUMP_IDENTIFIERS.ACCU_CHEK_INSIGHT.pumpSerial
|
||||
};
|
||||
bolus.identifier = opTools.calculateIdentifier(bolus);
|
||||
|
||||
let res3 = await self.instance.post(url, self.jwt.create)
|
||||
.send(bolus)
|
||||
.expect(201);
|
||||
identifiers.push(res3.body.identifier);
|
||||
self.cache.clear();
|
||||
|
||||
const uniqueIds = [...new Set(identifiers)];
|
||||
uniqueIds.length.should.equal(3);
|
||||
|
||||
for (const id of identifiers) {
|
||||
await deleteDoc(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('SGV Entry Pattern - high-frequency glucose readings', function() {
|
||||
const col = 'entries';
|
||||
const url = `/api/v3/${col}`;
|
||||
|
||||
async function deleteDoc(identifier) {
|
||||
self.cache.clear();
|
||||
await self.instance.delete(`${url}/${identifier}?permanent=true`, self.jwt.delete)
|
||||
.expect(200);
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
it('handles rapid sequential SGV entries with unique identifiers', async () => {
|
||||
const baseTime = Date.now();
|
||||
const device = 'test-device-sgv-' + baseTime;
|
||||
const identifiers = [];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const sgv = {
|
||||
device: device,
|
||||
date: baseTime + (i * 300000),
|
||||
app: testConst.TEST_APP,
|
||||
type: 'sgv',
|
||||
sgv: 120 + (i * 5),
|
||||
direction: ['Flat', 'FortyFiveUp', 'SingleUp'][i]
|
||||
};
|
||||
sgv.identifier = opTools.calculateIdentifier(sgv);
|
||||
|
||||
let res = await self.instance.post(url, self.jwt.create)
|
||||
.send(sgv)
|
||||
.expect(201);
|
||||
|
||||
res.body.status.should.equal(201);
|
||||
identifiers.push(res.body.identifier);
|
||||
self.cache.clear();
|
||||
}
|
||||
|
||||
const uniqueIds = [...new Set(identifiers)];
|
||||
uniqueIds.length.should.equal(3);
|
||||
|
||||
for (const id of identifiers) {
|
||||
await deleteDoc(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ var request = require('supertest');
|
||||
var should = require('should');
|
||||
var language = require('../lib/language')();
|
||||
|
||||
var aapsPatterns = require('./fixtures/aaps-patterns.json');
|
||||
|
||||
describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
|
||||
this.timeout(30000);
|
||||
var self = this;
|
||||
@@ -602,4 +604,191 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AAPS Sync Catch-up Simulation', function () {
|
||||
|
||||
beforeEach(function (done) {
|
||||
self.ctx.treatments.remove({ find: { eventType: { '$regex': '^Correction Bolus' } } }, function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
self.ctx.treatments.remove({ find: { eventType: { '$regex': '^Correction Bolus' } } }, function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles 50 rapid sequential SMB-style POSTs (AAPS offline recovery simulation)', function (done) {
|
||||
this.timeout(60000);
|
||||
var baseTime = Date.now();
|
||||
var successCount = 0;
|
||||
var errorCount = 0;
|
||||
var totalRequests = 50;
|
||||
var completed = 0;
|
||||
|
||||
function makeRequest(index) {
|
||||
var smb = {
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.1 + (Math.random() * 0.4),
|
||||
isSMB: true,
|
||||
pumpId: 10000 + index,
|
||||
pumpType: aapsPatterns.PUMP_IDENTIFIERS.ACCU_CHEK_INSIGHT.pumpType,
|
||||
pumpSerial: aapsPatterns.PUMP_IDENTIFIERS.ACCU_CHEK_INSIGHT.pumpSerial,
|
||||
created_at: new Date(baseTime + index * 300000).toISOString(),
|
||||
notes: 'AAPS sync catch-up ' + index
|
||||
};
|
||||
|
||||
request(self.app)
|
||||
.post('/api/treatments/')
|
||||
.set('api-secret', known)
|
||||
.send(smb)
|
||||
.end(function (err, res) {
|
||||
completed++;
|
||||
if (err || res.status !== 200) {
|
||||
errorCount++;
|
||||
} else {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
if (completed === totalRequests) {
|
||||
successCount.should.equal(totalRequests);
|
||||
errorCount.should.equal(0);
|
||||
|
||||
self.ctx.treatments.list({ find: { eventType: 'Correction Bolus' } }, function (listErr, list) {
|
||||
should.not.exist(listErr);
|
||||
list.length.should.be.greaterThanOrEqual(totalRequests);
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < totalRequests; i++) {
|
||||
setTimeout(makeRequest.bind(null, i), i * 5);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles 100 rapid sequential SGV-style POSTs to entries', function (done) {
|
||||
this.timeout(90000);
|
||||
var baseTime = Date.now();
|
||||
var successCount = 0;
|
||||
var errorCount = 0;
|
||||
var totalRequests = 100;
|
||||
var completed = 0;
|
||||
|
||||
function makeRequest(index) {
|
||||
var sgv = {
|
||||
type: 'sgv',
|
||||
sgv: 80 + Math.floor(Math.random() * 100),
|
||||
date: baseTime + index * 300000,
|
||||
dateString: new Date(baseTime + index * 300000).toISOString(),
|
||||
device: 'AndroidAPS-DexcomG6',
|
||||
direction: ['Flat', 'FortyFiveUp', 'SingleUp', 'FortyFiveDown'][index % 4]
|
||||
};
|
||||
|
||||
request(self.app)
|
||||
.post('/api/entries/')
|
||||
.set('api-secret', known)
|
||||
.send(sgv)
|
||||
.end(function (err, res) {
|
||||
completed++;
|
||||
if (err || res.status !== 200) {
|
||||
errorCount++;
|
||||
} else {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
if (completed === totalRequests) {
|
||||
successCount.should.equal(totalRequests);
|
||||
errorCount.should.equal(0);
|
||||
|
||||
self.ctx.entries.list({ find: { device: 'AndroidAPS-DexcomG6' } }, function (listErr, list) {
|
||||
should.not.exist(listErr);
|
||||
list.length.should.be.greaterThanOrEqual(totalRequests);
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < totalRequests; i++) {
|
||||
setTimeout(makeRequest.bind(null, i), i * 3);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles concurrent cross-collection sync (treatments + entries + devicestatus)', function (done) {
|
||||
this.timeout(60000);
|
||||
var baseTime = Date.now();
|
||||
var promises = [];
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
promises.push(
|
||||
new Promise(function (resolve, reject) {
|
||||
var idx = i;
|
||||
request(self.app)
|
||||
.post('/api/treatments/')
|
||||
.set('api-secret', known)
|
||||
.send({
|
||||
eventType: 'Correction Bolus',
|
||||
insulin: 0.2,
|
||||
created_at: new Date(baseTime + idx * 60000).toISOString(),
|
||||
notes: 'cross-collection treatment ' + idx
|
||||
})
|
||||
.end(function (err, res) {
|
||||
if (err) return reject(err);
|
||||
resolve({ type: 'treatment', status: res.status });
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
new Promise(function (resolve, reject) {
|
||||
var idx = i;
|
||||
request(self.app)
|
||||
.post('/api/entries/')
|
||||
.set('api-secret', known)
|
||||
.send({
|
||||
type: 'sgv',
|
||||
sgv: 100 + idx * 5,
|
||||
date: baseTime + idx * 60000,
|
||||
device: 'cross-collection-test'
|
||||
})
|
||||
.end(function (err, res) {
|
||||
if (err) return reject(err);
|
||||
resolve({ type: 'entry', status: res.status });
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
new Promise(function (resolve, reject) {
|
||||
var idx = i;
|
||||
request(self.app)
|
||||
.post('/api/devicestatus/')
|
||||
.set('api-secret', known)
|
||||
.send({
|
||||
device: 'cross-collection-device-' + idx,
|
||||
created_at: new Date(baseTime + idx * 60000).toISOString(),
|
||||
uploaderBattery: 90
|
||||
})
|
||||
.end(function (err, res) {
|
||||
if (err) return reject(err);
|
||||
resolve({ type: 'devicestatus', status: res.status });
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(function (results) {
|
||||
results.length.should.equal(30);
|
||||
results.forEach(function (result) {
|
||||
result.status.should.equal(200);
|
||||
});
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+271
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"COMMENT": "AAPS-realistic test fixtures based on AndroidAPS source code analysis",
|
||||
|
||||
"PUMP_IDENTIFIERS": {
|
||||
"ACCU_CHEK_INSIGHT": {
|
||||
"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH",
|
||||
"pumpSerial": "33013206"
|
||||
},
|
||||
"DANA_R": {
|
||||
"pumpType": "DANA_R",
|
||||
"pumpSerial": "ABC12345"
|
||||
},
|
||||
"OMNIPOD_EROS": {
|
||||
"pumpType": "OMNIPOD_EROS",
|
||||
"pumpSerial": "1234567890"
|
||||
}
|
||||
},
|
||||
|
||||
"SGV_ENTRIES": {
|
||||
"HIGH_FREQUENCY_BATCH": [
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": 120,
|
||||
"date": 1705600000000,
|
||||
"dateString": "2024-01-18T16:00:00.000Z",
|
||||
"device": "AndroidAPS-DexcomG6",
|
||||
"direction": "Flat",
|
||||
"utcOffset": 0,
|
||||
"app": "AAPS"
|
||||
},
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": 125,
|
||||
"date": 1705600300000,
|
||||
"dateString": "2024-01-18T16:05:00.000Z",
|
||||
"device": "AndroidAPS-DexcomG6",
|
||||
"direction": "FortyFiveUp",
|
||||
"utcOffset": 0,
|
||||
"app": "AAPS"
|
||||
},
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": 130,
|
||||
"date": 1705600600000,
|
||||
"dateString": "2024-01-18T16:10:00.000Z",
|
||||
"device": "AndroidAPS-DexcomG6",
|
||||
"direction": "SingleUp",
|
||||
"utcOffset": 0,
|
||||
"app": "AAPS"
|
||||
},
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": 128,
|
||||
"date": 1705600900000,
|
||||
"dateString": "2024-01-18T16:15:00.000Z",
|
||||
"device": "AndroidAPS-DexcomG6",
|
||||
"direction": "Flat",
|
||||
"utcOffset": 0,
|
||||
"app": "AAPS"
|
||||
},
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": 122,
|
||||
"date": 1705601200000,
|
||||
"dateString": "2024-01-18T16:20:00.000Z",
|
||||
"device": "AndroidAPS-DexcomG6",
|
||||
"direction": "FortyFiveDown",
|
||||
"utcOffset": 0,
|
||||
"app": "AAPS"
|
||||
}
|
||||
],
|
||||
"SINGLE_READING": {
|
||||
"type": "sgv",
|
||||
"sgv": 98,
|
||||
"date": 1549414398005,
|
||||
"dateString": "2019-02-06T01:53:18.005+0100",
|
||||
"device": "AndroidAPS-DexcomG6",
|
||||
"direction": "Flat",
|
||||
"utcOffset": 60,
|
||||
"app": "AAPS"
|
||||
}
|
||||
},
|
||||
|
||||
"SMB_BURSTS": {
|
||||
"COMMENT": "Super Micro Bolus - common in closed-loop systems, rapid sequential small corrections",
|
||||
"CORRECTION_SEQUENCE": [
|
||||
{
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.25,
|
||||
"isSMB": true,
|
||||
"pumpId": 4148,
|
||||
"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH",
|
||||
"pumpSerial": "33013206",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"type": "NORMAL",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
},
|
||||
{
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.30,
|
||||
"isSMB": true,
|
||||
"pumpId": 4149,
|
||||
"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH",
|
||||
"pumpSerial": "33013206",
|
||||
"date": 1705600300000,
|
||||
"created_at": "2024-01-18T16:05:00.000Z",
|
||||
"type": "NORMAL",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
},
|
||||
{
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.15,
|
||||
"isSMB": true,
|
||||
"pumpId": 4150,
|
||||
"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH",
|
||||
"pumpSerial": "33013206",
|
||||
"date": 1705600600000,
|
||||
"created_at": "2024-01-18T16:10:00.000Z",
|
||||
"type": "NORMAL",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"MEAL_SCENARIO": {
|
||||
"COMMENT": "Typical meal with carbs entry, bolus wizard, and meal bolus",
|
||||
"CARB_ENTRY": {
|
||||
"eventType": "Carb Correction",
|
||||
"carbs": 45,
|
||||
"date": 1626200383325,
|
||||
"created_at": "2021-07-13T17:59:43.325Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
},
|
||||
"MEAL_BOLUS": {
|
||||
"eventType": "Meal Bolus",
|
||||
"insulin": 8.1,
|
||||
"pumpId": 4102,
|
||||
"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH",
|
||||
"pumpSerial": "33013206",
|
||||
"date": 1626175512664,
|
||||
"created_at": "2021-07-13T11:25:12.664Z",
|
||||
"type": "NORMAL",
|
||||
"isValid": true,
|
||||
"isSMB": false,
|
||||
"app": "AAPS"
|
||||
},
|
||||
"BOLUS_WIZARD": {
|
||||
"eventType": "Bolus Wizard",
|
||||
"glucose": 134,
|
||||
"glucoseType": "Sensor",
|
||||
"carbs": 45,
|
||||
"insulin": 8.1,
|
||||
"date": 1626175500000,
|
||||
"created_at": "2021-07-13T11:25:00.000Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
}
|
||||
},
|
||||
|
||||
"DEDUPLICATION_TESTS": {
|
||||
"COMMENT": "Test cases for duplicate detection using pump composite key",
|
||||
"ORIGINAL": {
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.5,
|
||||
"pumpId": 4148,
|
||||
"pumpType": "DANA_R",
|
||||
"pumpSerial": "12345",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
},
|
||||
"DUPLICATE_SAME_PUMP_KEY": {
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.5,
|
||||
"pumpId": 4148,
|
||||
"pumpType": "DANA_R",
|
||||
"pumpSerial": "12345",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
},
|
||||
"DIFFERENT_PUMP_ID": {
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.5,
|
||||
"pumpId": 4149,
|
||||
"pumpType": "DANA_R",
|
||||
"pumpSerial": "12345",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
},
|
||||
"DIFFERENT_PUMP_SERIAL": {
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.5,
|
||||
"pumpId": 4148,
|
||||
"pumpType": "DANA_R",
|
||||
"pumpSerial": "99999",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
}
|
||||
},
|
||||
|
||||
"TEMP_BASAL": {
|
||||
"COMMENT": "Temporary basal rate changes from closed-loop",
|
||||
"SAMPLE": {
|
||||
"eventType": "Temp Basal",
|
||||
"duration": 30,
|
||||
"absolute": 1.2,
|
||||
"rate": 1.2,
|
||||
"pumpId": 5001,
|
||||
"pumpType": "OMNIPOD_EROS",
|
||||
"pumpSerial": "1234567890",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"isValid": true,
|
||||
"app": "AAPS"
|
||||
}
|
||||
},
|
||||
|
||||
"DEVICESTATUS": {
|
||||
"COMMENT": "Device status uploads with pump/loop state",
|
||||
"SAMPLE": {
|
||||
"device": "AndroidAPS",
|
||||
"date": 1705600000000,
|
||||
"created_at": "2024-01-18T16:00:00.000Z",
|
||||
"pump": {
|
||||
"battery": {
|
||||
"percent": 75
|
||||
},
|
||||
"reservoir": 120.5,
|
||||
"status": {
|
||||
"status": "normal",
|
||||
"timestamp": "2024-01-18T16:00:00.000Z"
|
||||
}
|
||||
},
|
||||
"openaps": {
|
||||
"enacted": {
|
||||
"timestamp": "2024-01-18T16:00:00.000Z",
|
||||
"rate": 1.2,
|
||||
"duration": 30,
|
||||
"received": true
|
||||
},
|
||||
"iob": {
|
||||
"iob": 2.5,
|
||||
"basaliob": 1.2,
|
||||
"activity": 0.05,
|
||||
"time": "2024-01-18T16:00:00.000Z"
|
||||
}
|
||||
},
|
||||
"uploaderBattery": 85,
|
||||
"app": "AAPS"
|
||||
}
|
||||
},
|
||||
|
||||
"SYNC_CATCHUP_SCENARIO": {
|
||||
"COMMENT": "Simulates AAPS catching up after being offline - many records in quick succession",
|
||||
"RECORD_COUNT": 100,
|
||||
"BASE_TIMESTAMP": 1705600000000,
|
||||
"INTERVAL_MS": 300000
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user