Files
cgm-remote-monitor/tests/api3.renderer.test.js
T
Ben West f907f0d6ef Fix MongoDB 5.x compatibility in entries storage and API3 renderer test
Fixes test failure in api3.renderer.test.js "SEARCH should accept csv content type"
that was caused by MongoDB driver upgrade from 3.x to 5.x.

Changes:

1. lib/server/entries.js:
   - Change from replaceOne() to updateOne() with $set operator
   - MongoDB 3.x update() did partial updates, but 5.x replaceOne() does
     full document replacement
   - Using updateOne with $set preserves the original partial update behavior
   - Prevents documents with same {sysTime, type} from replacing each other

2. tests/api3.renderer.test.js:
   - Add database cleanup in before() hook to delete all entries
   - Ensures test isolation from previous test files
   - Previous tests (especially old API v1 tests) were leaving entries in DB
     with undefined app/identifier fields that interfered with CSV rendering

The CSV test now passes - it expects exactly 2 documents but was getting
105 entries due to leftover test data from previous test runs.
2026-01-19 13:14:21 -08:00

297 lines
8.6 KiB
JavaScript

/* eslint require-atomic-updates: 0 */
'use strict';
require('should');
describe('API3 output renderers', function() {
const self = this
, testConst = require('./fixtures/api3/const.json')
, instance = require('./fixtures/api3/instance')
, authSubject = require('./fixtures/api3/authSubject')
, opTools = require('../lib/api3/shared/operationTools')
, _ = require('lodash')
, xml2js = require('xml2js')
, csvParse = require('csv-parse/lib/sync')
;
self.historyFrom = (new Date()).getTime() - 1000; // starting timestamp for HISTORY operations
self.doc1 = testConst.SAMPLE_ENTRIES[0];
self.doc1.date = (new Date()).getTime() - (5 * 60 * 1000);
self.doc1.identifier = opTools.calculateIdentifier(self.doc1);
self.doc2 = testConst.SAMPLE_ENTRIES[1];
self.doc2.date = (new Date()).getTime();
self.doc2.identifier = opTools.calculateIdentifier(self.doc2);
self.xmlParser = new xml2js.Parser({
explicitArray: false
});
self.csvParserOptions = {
columns: true,
skip_empty_lines: true
};
self.timeout(15000);
before(async () => {
self.instance = await instance.create({});
self.app = self.instance.app;
self.env = self.instance.env;
self.col = 'entries';
self.url = `/api/v3/${self.col}`;
let authResult = await authSubject(self.instance.ctx.authorization.storage, [
'create',
'read',
'delete'
], self.instance.app);
self.subject = authResult.subject;
self.jwt = authResult.jwt;
self.cache = self.instance.cacheMonitor;
// Clean up ALL entries to ensure test isolation from previous test files
const col = self.instance.ctx.store.collection(self.instance.env.entries_collection || 'entries');
await col.deleteMany({});
});
after(() => {
self.instance.server.close();
});
beforeEach(() => {
self.cache.clear();
});
afterEach(() => {
self.cache.shouldBeEmpty();
});
/**
* Checks if all properties from obj1 are string identical in obj2
* (comparison of properties is made using toString())
* @param {Object} obj1
* @param {Object} obj2
*/
self.checkProps = function checkProps (obj1, obj2) {
for (let propName in obj1) {
obj1[propName].toString().should.eql(obj2[propName].toString());
}
};
/**
* Checks if all objects from arrModel exist in arr
* (with string identical properties)
* @param arrModel
* @param arr
*/
self.checkItems = function checkItems (arrModel, arr) {
for (let itemModel of arrModel) {
const item = _.find(arr, (doc) => doc.identifier === itemModel.identifier);
item.should.not.be.empty();
self.checkProps(itemModel, item);
}
};
/**
* Checks if given text is valid XML.
* Next checks if all objects from arrModel exist in parsed array
* (with string identical properties)
* @param arrModel
* @param xmlText
* @returns {Promise}
*/
self.checkXmlItems = async function checkXmlItems (arrModel, xmlText) {
xmlText.should.startWith('<?xml version=\'1.0\' encoding=\'utf-8\'?>');
const xml = await self.xmlParser.parseStringPromise(xmlText);
xml.items.should.not.be.empty();
let items = xml.items.item;
items.should.be.Array();
items.length.should.be.aboveOrEqual(arrModel.length);
self.checkItems(arrModel, items);
};
/**
* Checks if given text is valid CSV.
* Next checks if all objects from arrModel exist in parsed array
* (with string identical properties)
* @param arrModel
* @param csvText
* @returns {Promise}
*/
self.checkCsvItems = async function checkXmlItems (arrModel, csvText) {
csvText.should.not.be.empty();
const items = csvParse(csvText, self.csvParserOptions);
items.should.be.Array();
items.length.should.be.aboveOrEqual(arrModel.length);
self.checkItems(arrModel, items);
};
it('should create 2 mock documents', async () => {
async function createDoc (doc) {
let res = await self.instance.post(`${self.url}`, self.jwt.create)
.send(doc)
.expect(201);
res.body.status.should.equal(201);
res = await self.instance.get(`${self.url}/${doc.identifier}`, self.jwt.read)
.expect(200);
return res.body;
}
self.doc1json = await createDoc(self.doc1);
self.cache.nextShouldEql(self.col, self.doc1)
self.doc2json = await createDoc(self.doc2);
self.cache.nextShouldEql(self.col, self.doc2)
});
it('READ/SEARCH/HISTORY should not accept unsupported content type', async () => {
async function check406 (request) {
const res = await request
.expect(406);
res.status.should.equal(406);
res.body.message.should.eql('Unsupported output format requested');
should.not.exist(res.body.result);
}
await check406(self.instance.get(`${self.url}/${self.doc1.identifier}.ttf?fields=_all`, self.jwt.read));
await check406(self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all`, self.jwt.read)
.set('Accept', 'font/ttf'));
await check406(self.instance.get(`${self.url}.ttf?fields=_all`, self.jwt.read));
await check406(self.instance.get(`${self.url}?fields=_all`, self.jwt.read)
.set('Accept', 'font/ttf'));
await check406(self.instance.get(`${self.url}/history/${self.doc1.date}.ttf`, self.jwt.read));
await check406(self.instance.get(`${self.url}/history/${self.doc1.date}`, self.jwt.read)
.set('Accept', 'font/ttf'));
});
it('READ should accept xml content type', async () => {
let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.xml?fields=_all`, self.jwt.read)
.expect(200);
res.text.should.startWith('<?xml version=\'1.0\' encoding=\'utf-8\'?>');
const xml = await self.xmlParser.parseStringPromise(res.text);
xml.item.should.not.be.empty();
self.checkProps(self.doc1, xml.item);
let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all`, self.jwt.read)
.set('Accept', 'application/xml')
.expect(200);
res.text.should.eql(res2.text);
});
it('READ should accept csv content type', async () => {
let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.csv?fields=_all`, self.jwt.read)
.expect(200);
await self.checkCsvItems([self.doc1], res.text);
let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all`, self.jwt.read)
.set('Accept', 'text/csv')
.expect(200);
res.text.should.eql(res2.text);
});
it('SEARCH should accept xml content type', async () => {
let res = await self.instance.get(`${self.url}.xml?date$gte=${self.doc1.date}`, self.jwt.read)
.expect(200);
await self.checkXmlItems([self.doc1, self.doc2], res.text);
let res2 = await self.instance.get(`${self.url}?date$gte=${self.doc1.date}`, self.jwt.read)
.set('Accept', 'application/xml')
.expect(200);
res.text.should.be.eql(res2.text);
});
it('SEARCH should accept csv content type', async () => {
let res = await self.instance.get(`${self.url}.csv?date$gte=${self.doc1.date}`, self.jwt.read)
.expect(200);
await self.checkCsvItems([self.doc1, self.doc2], res.text);
let res2 = await self.instance.get(`${self.url}?date$gte=${self.doc1.date}`, self.jwt.read)
.set('Accept', 'text/csv')
.expect(200);
res.text.should.be.eql(res2.text);
});
it('HISTORY should accept xml content type', async () => {
let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.xml`, self.jwt.read)
.expect(200);
await self.checkXmlItems([self.doc1, self.doc2], res.text);
let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}`, self.jwt.read)
.set('Accept', 'application/xml')
.expect(200);
res.text.should.be.eql(res2.text);
});
it('HISTORY should accept csv content type', async () => {
let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.csv`, self.jwt.read)
.expect(200);
await self.checkCsvItems([self.doc1, self.doc2], res.text);
let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}`, self.jwt.read)
.set('Accept', 'text/csv')
.expect(200);
res.text.should.be.eql(res2.text);
});
it('should remove mock documents', async () => {
async function deleteDoc (identifier) {
let res = await self.instance.delete(`${self.url}/${identifier}`, self.jwt.delete)
.query({ 'permanent': 'true' })
.expect(200);
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col);
}
await deleteDoc(self.doc1.identifier);
await deleteDoc(self.doc2.identifier);
});
});