APIv3: wrapping all results in JSON (#6703)

* APIv3: isolating documents from tests (not allowing clashes of calculated identifiers)

* removing unused async keyword

* fixing api v3 swagger and moving it to /api3-docs

* APIv3: adding cachedCollection stub of cachedCollection storage implementation

* APIv3: mongo cachedCollection storage implementation

* APIv3: testing and debugging cache updates

* APIv3: more testing on cache updates

* APIv3: fixing bad async functions

* APIv3: finishing cache invalidation tests

* APIv3: wrapping VERSION result

* APIv3: wrapping STATUS result

* APIv3: wrapping DELETE result

* APIv3: wrapping READ result + partially SEARCH and HISTORY

* APIv3: wrapping CREATE result

* APIv3: wrapping UPDATE + PATCH result

* APIv3: wrapping LAST MODIFIED result

* APIv3: updating swagger doc

* APIv3: updating tutorial.md

* APIv3: tuning tests

* APIv3: merge dev

Co-authored-by: Petr Ondrusek <petr.ondrusek@seznam.cz>
Co-authored-by: Petr Ondrůšek <petr.ondrusek@okin.eu>
Co-authored-by: Sulka Haro <sulka@sulka.net>
This commit is contained in:
Petr Ondrusek
2021-01-07 22:58:23 +02:00
committed by GitHub
co-authored by Petr Ondrusek Petr Ondrůšek Sulka Haro
parent 914ba78f36
commit cfdbaa8bbd
28 changed files with 1557 additions and 581 deletions
+3 -2
View File
@@ -1,5 +1,5 @@
{
"API3_VERSION": "3.0.0-alpha",
"API3_VERSION": "3.0.2-alpha",
"API3_SECURITY_ENABLE": true,
"API3_TIME_SKEW_TOLERANCE": 5,
"API3_DEDUP_FALLBACK_ENABLED": true,
@@ -41,6 +41,7 @@
"HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT",
"HTTP_403_MISSING_PERMISSION": "Missing permission {0}",
"HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS",
"HTTP_404_BAD_OPERATION": "Bad operation or collection",
"HTTP_406_UNSUPPORTED_FORMAT": "Unsupported output format requested",
"HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document",
"HTTP_500_INTERNAL_ERROR": "Internal Server Error",
@@ -52,4 +53,4 @@
"MIN_TIMESTAMP": 946684800000,
"MIN_UTC_OFFSET": -1440,
"MAX_UTC_OFFSET": 1440
}
}
+28 -25
View File
@@ -18,31 +18,34 @@ The server replies with `406 Not Acceptable` HTTP status in case of not supporte
Default content type is JSON, output can look like this:
```
[
{
"type":"sgv",
"sgv":"171",
"dateString":"2014-07-19T02:44:15.000-07:00",
"date":1405763055000,
"device":"dexcom",
"direction":"Flat",
"identifier":"5c5a2404e0196f4d3d9a718a",
"srvModified":1405763055000,
"srvCreated":1405763055000
},
{
"type":"sgv",
"sgv":"176",
"dateString":"2014-07-19T03:09:15.000-07:00",
"date":1405764555000,
"device":"dexcom",
"direction":"Flat",
"identifier":"5c5a2404e0196f4d3d9a7187",
"srvModified":1405764555000,
"srvCreated":1405764555000
}
]
```json
{
"status": 200,
"result": [
{
"type": "sgv",
"sgv": "171",
"dateString": "2014-07-19T02:44:15.000-07:00",
"date": 1405763055000,
"device": "dexcom",
"direction": "Flat",
"identifier": "5c5a2404e0196f4d3d9a718a",
"srvModified": 1405763055000,
"srvCreated": 1405763055000
},
{
"type": "sgv",
"sgv": "176",
"dateString": "2014-07-19T03:09:15.000-07:00",
"date": 1405764555000,
"device": "dexcom",
"direction": "Flat",
"identifier": "5c5a2404e0196f4d3d9a7187",
"srvModified": 1405764555000,
"srvCreated": 1405764555000
}
]
}
```
### XML
+134 -106
View File
@@ -22,15 +22,18 @@ request('https://nsapiv3.herokuapp.com/api/v3/version',
(error, response, body) => console.log(body));
```
Sample result:
```javascript
{
"version":"0.12.2",
"apiVersion":"3.0.0-alpha",
"srvDate":1564386001772,
"storage":{
"storage":"mongodb",
"version":"3.6.12"
}
```json
{
"status": 200,
"result": {
"version": "14.1.0",
"apiVersion": "3.0.2-alpha",
"srvDate": 1609402081548,
"storage": {
"storage": "mongodb",
"version": "4.2.11"
}
}
}
```
@@ -50,22 +53,25 @@ request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`,
(error, response, body) => console.log(body));
```
Sample result:
```javascript
{
"version":"0.12.2",
"apiVersion":"3.0.0-alpha",
"srvDate":1564391740738,
"storage":{
"storage":"mongodb",
"version":"3.6.12"
},
"apiPermissions":{
"devicestatus":"crud",
"entries":"crud",
"food":"crud",
"profile":"crud",
"settings":"crud",
"treatments":"crud"
```json
{
"status": 200,
"result": {
"version": "14.1.0",
"apiVersion": "3.0.2-alpha",
"srvDate": 1609427571833,
"storage": {
"storage": "mongodb",
"version": "4.2.11"
},
"apiPermissions": {
"devicestatus": "crud",
"entries": "crud",
"food": "crud",
"profile": "crud",
"settings": "crud",
"treatments": "crud"
}
}
}
```
@@ -86,24 +92,27 @@ request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&lim
(error, response, body) => console.log(body));
```
Sample result:
```
[
{
"dateString":"2019-07-30T02:24:50.434+0200",
"sgv":115,
"direction":"FortyFiveDown"
},
{
"dateString":"2019-07-30T02:19:50.374+0200",
"sgv":121,
"direction":"FortyFiveDown"
},
{
"dateString":"2019-07-30T02:14:50.450+0200",
"sgv":129,
"direction":"FortyFiveDown"
}
]
```json
{
"status": 200,
"result": [
{
"dateString": "2019-07-30T02:24:50.434+0200",
"sgv": 115,
"direction": "FortyFiveDown"
},
{
"dateString": "2019-07-30T02:19:50.374+0200",
"sgv": 121,
"direction": "FortyFiveDown"
},
{
"dateString": "2019-07-30T02:14:50.450+0200",
"sgv": 129,
"direction": "FortyFiveDown"
}
]
}
```
@@ -129,11 +138,15 @@ request({
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}`
},
(error, response, body) => console.log(response.headers.location));
(error, response, body) => console.log(body));
```
Sample result:
```
/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895
```json
{
"status": 201,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"lastModified": 1564591511711
}
```
@@ -152,19 +165,22 @@ request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`,
(error, response, body) => console.log(body));
```
Sample result:
```
{
"date":1564591511232,
"app":"AndroidAPS",
"device":"Samsung XCover 4-861536030196001",
"eventType":"Correction Bolus",
"insulin":0.3,
"identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"utcOffset":0,
"created_at":"2019-07-31T16:45:11.232Z",
"srvModified":1564591627732,
"srvCreated":1564591511711,
"subject":"test-admin"
```json
{
"status": 200,
"result": {
"date": 1564591511232,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus",
"insulin": 0.3,
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"utcOffset": 0,
"created_at": "2019-07-31T16:45:11.232Z",
"srvModified": 1564591627732,
"srvCreated": 1564591511711,
"subject": "test-admin"
}
}
```
@@ -183,14 +199,17 @@ request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`,
(error, response, body) => console.log(body));
```
Sample result:
```javascript
```json
{
"srvDate":1564591783202,
"collections":{
"devicestatus":1564591490074,
"entries":1564591486801,
"profile":1548524042744,
"treatments":1564591627732
"status": 200,
"result": {
"srvDate": 1564591783202,
"collections": {
"devicestatus": 1564591490074,
"entries": 1564591486801,
"profile": 1548524042744,
"treatments": 1564591627732
}
}
}
```
@@ -220,11 +239,13 @@ request({
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(response.statusCode));
(error, response, body) => console.log(body));
```
Sample result:
```
204
```json
{
"status": 200
}
```
@@ -248,11 +269,13 @@ request({
json: true,
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(response.statusCode));
(error, response, body) => console.log(body));
```
Sample result:
```
204
```json
{
"status": 200
}
```
@@ -271,11 +294,13 @@ request({
method: 'delete',
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
},
(error, response, body) => console.log(response.statusCode));
(error, response, body) => console.log(body));
```
Sample result:
```
204
```json
{
"status": 200
}
```
@@ -294,36 +319,39 @@ request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}
(error, response, body) => console.log(response.body));
```
Sample result:
```
[
{
"date":1564521267421,
"app":"AndroidAPS",
"device":"Samsung XCover 4-861536030196001",
"eventType":"Correction Bolus",
"insulin":0.5,
"utcOffset":0,
"created_at":"2019-07-30T21:14:27.421Z",
"identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"srvModified":1564592440416,
"srvCreated":1564592334853,
"subject":"test-admin",
"modifiedBy":"test-admin",
"isValid":false
},
{
"date":1564592545299,
"app":"AndroidAPS",
"device":"Samsung XCover 4-861536030196001",
"eventType":"Snack Bolus",
"carbs":10,
"identifier":"267c43c2-f629-5191-a542-4f410c69e486",
"utcOffset":0,
"created_at":"2019-07-31T17:02:25.299Z",
"srvModified":1564592545781,
"srvCreated":1564592545781,
"subject":"test-admin"
}
]
```json
{
"status": 200,
"result": [
{
"date": 1564521267421,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Correction Bolus",
"insulin": 0.5,
"utcOffset": 0,
"created_at": "2019-07-30T21:14:27.421Z",
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
"srvModified": 1564592440416,
"srvCreated": 1564592334853,
"subject": "test-admin",
"modifiedBy": "test-admin",
"isValid": false
},
{
"date": 1564592545299,
"app": "AndroidAPS",
"device": "Samsung XCover 4-861536030196001",
"eventType": "Snack Bolus",
"carbs": 10,
"identifier": "267c43c2-f629-5191-a542-4f410c69e486",
"utcOffset": 0,
"created_at": "2019-07-31T17:02:25.299Z",
"srvModified": 1564592545781,
"srvCreated": 1564592545781,
"subject": "test-admin"
}
]
}
```
Notice the `"isValid":false` field marking the deletion of the document.
+9 -2
View File
@@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
, security = require('../../security')
, validate = require('./validate.js')
, path = require('path')
, opTools = require('../../shared/operationTools')
;
/**
@@ -35,7 +36,13 @@ async function insert (opCtx, doc) {
res.setHeader('Last-Modified', now.toUTCString());
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier));
res.status(apiConst.HTTP.CREATED).send({ });
const fields = {
identifier: identifier,
lastModified: now.getTime()
};
opTools.sendJSON({ res, status: apiConst.HTTP.CREATED, fields: fields });
ctx.bus.emit('storage-socket-create', { colName: col.colName, doc });
col.autoPrune();
@@ -43,4 +50,4 @@ async function insert (opCtx, doc) {
}
module.exports = insert;
module.exports = insert;
+6 -6
View File
@@ -36,7 +36,7 @@ async function validateDelete (opCtx) {
throw new Error('empty result');
if (result.length === 0) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
else {
const storageDoc = result[0];
@@ -62,13 +62,13 @@ async function deletePermanently (opCtx) {
throw new Error('empty result');
if (!result.deleted) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
col.autoPrune();
ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier });
ctx.bus.emit('data-received');
return res.status(apiConst.HTTP.NO_CONTENT).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.OK });
}
@@ -89,13 +89,13 @@ async function markAsDeleted (opCtx) {
throw new Error('empty result');
if (!result.updated) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier });
col.autoPrune();
ctx.bus.emit('data-received');
return res.status(apiConst.HTTP.NO_CONTENT).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.OK });
}
@@ -119,4 +119,4 @@ function deleteOperation (ctx, env, app, col) {
};
}
module.exports = deleteOperation;
module.exports = deleteOperation;
+2 -1
View File
@@ -39,7 +39,8 @@ async function history (opCtx, fieldsProjector) {
throw new Error('empty result');
if (result.length === 0) {
return res.status(apiConst.HTTP.NO_CONTENT).end();
res.status(apiConst.HTTP.OK);
return renderer.render(res, result);
}
_.each(result, col.resolveDates);
+5 -5
View File
@@ -36,7 +36,7 @@ async function patch (opCtx) {
const storageDoc = result[0];
if (storageDoc.isValid === false) {
return res.status(apiConst.HTTP.GONE).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.GONE);
}
const modifiedDate = col.resolveDates(storageDoc)
@@ -44,13 +44,13 @@ async function patch (opCtx) {
if (ifUnmodifiedSince
&& dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) {
return res.status(apiConst.HTTP.PRECONDITION_FAILED).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED);
}
await applyPatch(opCtx, identifier, doc, storageDoc);
}
else {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND);
}
}
@@ -82,7 +82,7 @@ async function applyPatch (opCtx, identifier, doc, storageDoc) {
throw new Error('matchedCount empty');
res.setHeader('Last-Modified', now.toUTCString());
res.status(apiConst.HTTP.NO_CONTENT).send({ });
opTools.sendJSONStatus(res, apiConst.HTTP.OK);
const fieldsProjector = new FieldsProjector('_all');
const patchedDocs = await col.storage.findOne(identifier, fieldsProjector);
@@ -115,4 +115,4 @@ function patchOperation (ctx, env, app, col) {
};
}
module.exports = patchOperation;
module.exports = patchOperation;
+3 -3
View File
@@ -26,12 +26,12 @@ async function read (opCtx) {
throw new Error('empty result');
if (result.length === 0) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
}
const doc = result[0];
if (doc.isValid === false) {
return res.status(apiConst.HTTP.GONE).end();
return opTools.sendJSON({ res, status: apiConst.HTTP.GONE });
}
@@ -74,4 +74,4 @@ function readOperation (ctx, env, app, col) {
};
}
module.exports = readOperation;
module.exports = readOperation;
+3 -3
View File
@@ -48,7 +48,7 @@ async function updateConditional (opCtx, doc, storageDoc) {
const { col, req, res } = opCtx;
if (storageDoc.isValid === false) {
return res.status(apiConst.HTTP.GONE).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.GONE);
}
const modifiedDate = col.resolveDates(storageDoc)
@@ -56,7 +56,7 @@ async function updateConditional (opCtx, doc, storageDoc) {
if (ifUnmodifiedSince
&& dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) {
return res.status(apiConst.HTTP.PRECONDITION_FAILED).end();
return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED);
}
await replace(opCtx, doc, storageDoc);
@@ -83,4 +83,4 @@ function updateOperation (ctx, env, app, col) {
};
}
module.exports = updateOperation;
module.exports = updateOperation;
+11 -2
View File
@@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
, security = require('../../security')
, validate = require('./validate.js')
, path = require('path')
, opTools = require('../../shared/operationTools')
;
/**
@@ -37,12 +38,20 @@ async function replace (opCtx, doc, storageDoc, options) {
throw new Error('empty matchedCount');
res.setHeader('Last-Modified', now.toUTCString());
const fields = {
lastModified: now.getTime()
}
if (storageDoc.identifier !== doc.identifier || isDeduplication) {
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier));
fields.identifier = doc.identifier;
fields.isDeduplication = true;
if (storageDoc.identifier !== doc.identifier) {
fields.deduplicatedIdentifier = storageDoc.identifier;
}
}
res.status(apiConst.HTTP.NO_CONTENT).send({ });
opTools.sendJSON({ res, status: apiConst.HTTP.OK, fields });
ctx.bus.emit('storage-socket-update', { colName: col.colName, doc });
col.autoPrune();
@@ -50,4 +59,4 @@ async function replace (opCtx, doc, storageDoc, options) {
}
module.exports = replace;
module.exports = replace;
+5
View File
@@ -7,6 +7,7 @@ const express = require('express')
, apiConst = require('./const.json')
, security = require('./security')
, genericSetup = require('./generic/setup')
, opTools = require('./shared/operationTools')
;
function configure (env, ctx) {
@@ -104,6 +105,10 @@ function configure (env, ctx) {
res.redirect(307, '../../../api3-docs');
});
app.use((req, res) => {
opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND, apiConst.MSG.HTTP_404_BAD_OPERATION);
})
ctx.storageSocket = new StorageSocket(app, env, ctx);
return app;
+29 -5
View File
@@ -6,18 +6,41 @@ const apiConst = require('../const.json')
, uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-)
;
function sendJSON ({ res, result, status, fields }) {
const json = {
status: status || apiConst.HTTP.OK,
result: result
};
if (result) {
json.result = result
}
if (fields) {
Object.assign(json, fields);
}
res.status(json.status).json(json);
}
function sendJSONStatus (res, status, title, description, warning) {
const json = {
status: status,
message: title,
description: description
status: status
};
if (title) { json.message = title }
if (description) { json.description = description }
// Add optional warning message.
if (warning) { json.warning = warning; }
res.status(status).json(json);
res.status(status)
.json(json);
return title;
}
@@ -104,8 +127,9 @@ function resolveIdentifier (doc) {
module.exports = {
sendJSON,
sendJSONStatus,
validateCommon,
calculateIdentifier,
resolveIdentifier
};
};
+15 -2
View File
@@ -53,7 +53,7 @@ function extension2accept (req, res, next) {
*/
function render (res, data) {
res.format({
'json': () => res.send(data),
'json': () => renderJson(res, data),
'csv': () => renderCsv(res, data),
'xml': () => renderXml(res, data),
'default': () =>
@@ -62,6 +62,19 @@ function render (res, data) {
}
/**
* Format data to output as JSON
* @param {Object} res
* @param {any} data
*/
function renderJson (res, data) {
res.send({
status: apiConst.HTTP.OK,
result: data
});
}
/**
* Format data to output as .csv
* @param {Object} res
@@ -96,4 +109,4 @@ function renderXml (res, data) {
module.exports = {
extension2accept,
render
};
};
+2 -2
View File
@@ -38,7 +38,7 @@ function configure (app, ctx, env) {
return { colName: col.colName, lastModified: result };
}
async function collectionsAsync (auth) {
const cols = app.get('collections')
@@ -77,7 +77,7 @@ function configure (app, ctx, env) {
info.collections = await collectionsAsync(auth);
res.json(info);
opTools.sendJSON({ res, result: info });
}
+1 -1
View File
@@ -47,7 +47,7 @@ function configure (app, ctx, env) {
}
}
res.json(info);
opTools.sendJSON({ res, result: info });
}
+1 -1
View File
@@ -13,7 +13,7 @@ function configure (app) {
try {
const versionInfo = await storageTools.getVersionInfo(app);
res.json(versionInfo);
opTools.sendJSON({ res, result: versionInfo });
} catch(error) {
console.error(error);
+853 -212
View File
File diff suppressed because it is too large Load Diff
+235 -65
View File
@@ -2,7 +2,7 @@ openapi: 3.0.0
servers:
- url: '/api/v3'
info:
version: "3.0.1"
version: "3.0.2"
title: Nightscout API
contact:
name: NS development discussion channel
@@ -102,7 +102,7 @@ paths:
3) paging - using `limit` and `skip` parameters
When there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content.
If successful, HTTP 200 code is returned with JSON array of matching documents as a response content (it may be empty).
This operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`).
@@ -120,13 +120,12 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/search200'
204:
$ref: '#/components/responses/search204'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -145,10 +144,11 @@ paths:
- generic
summary: 'CREATE: Inserts a new document into the collection'
description:
Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header.
Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified.
`identifier` is included in response body or it can be parsed from the `Location` response header.
When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers.
When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 200 HTTP status code along with `Last-Modified` and correct `Location` headers. The response body then includes `isDeduplication`=`true` and `deduplicatedIdentifier` fields.
This operation provides autopruning of the collection (if autopruning is enabled).
@@ -165,13 +165,14 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/200Deduplication'
201:
$ref: '#/components/responses/201CreatedLocation'
204:
$ref: '#/components/responses/204NoContentLocation'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -215,7 +216,7 @@ paths:
Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
When `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side.
@@ -229,7 +230,8 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
@@ -254,10 +256,10 @@ paths:
- generic
summary: 'UPDATE: Updates a document in the collection'
description:
Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body.
Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 200 HTTP status code will be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
When no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter).
@@ -283,13 +285,14 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/200Ok'
201:
$ref: '#/components/responses/201Created'
204:
$ref: '#/components/responses/204NoContentLocation'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -312,10 +315,10 @@ paths:
- generic
summary: 'PATCH: Partially updates document in the collection'
description:
Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body.
Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 200 HTTP status code will be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code.
@@ -347,11 +350,12 @@ paths:
$ref: '#/components/schemas/DocumentToPost'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
204:
$ref: '#/components/responses/204NoContentLocation'
200:
$ref: '#/components/responses/200Ok'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -360,10 +364,10 @@ paths:
$ref: '#/components/responses/403Forbidden'
404:
$ref: '#/components/responses/404NotFound'
412:
$ref: '#/components/responses/412PreconditionFailed'
410:
$ref: '#/components/responses/410Gone'
412:
$ref: '#/components/responses/412PreconditionFailed'
422:
$ref: '#/components/responses/422UnprocessableEntity'
@@ -387,11 +391,12 @@ paths:
- $ref: '#/components/parameters/permanentParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
204:
description: Successful operation - empty response
200:
$ref: '#/components/responses/200Ok'
401:
$ref: '#/components/responses/401Unauthorized'
403:
@@ -430,9 +435,6 @@ paths:
Deleted documents will appear with `isValid` = `false` field.
When there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content.
HISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections).
@@ -448,13 +450,12 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/history200'
204:
$ref: '#/components/responses/history204'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -509,13 +510,12 @@ paths:
- $ref: '#/components/parameters/fieldsParam'
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
$ref: '#/components/responses/history200'
204:
$ref: '#/components/responses/history204'
400:
$ref: '#/components/responses/400BadRequest'
401:
@@ -556,7 +556,8 @@ paths:
This operation requires authorization in contrast with VERSION operation.
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
@@ -591,7 +592,8 @@ paths:
This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to.
security:
- apiKeyAuth: []
- accessToken: []
- jwtoken: []
responses:
200:
@@ -850,11 +852,54 @@ components:
######################################################################################
responses:
200Ok:
description: The request was successfully processed
content:
application/json:
schema:
properties:
status:
type: integer
example: 200
200Deduplication:
description: Successfully updated a duplicate document in the collection
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
'Location':
$ref: '#/components/schemas/headerLocation'
content:
application/json:
schema:
properties:
status:
type: integer
example: 200
identifier:
$ref: '#/components/schemas/identifierField'
isDeduplication:
$ref: '#/components/schemas/isDeduplicationField'
deduplicatedIdentifier:
$ref: '#/components/schemas/deduplicatedIdentifierField'
201Created:
description: Successfully created a new document in collection
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
content:
application/json:
schema:
properties:
status:
type: integer
example: 201
identifier:
$ref: '#/components/schemas/identifierField'
lastModified:
$ref: '#/components/schemas/lastModifiedField'
201CreatedLocation:
description: Successfully created a new document in collection
@@ -863,20 +908,17 @@ components:
$ref: '#/components/schemas/headerLastModified'
'Location':
$ref: '#/components/schemas/headerLocation'
204NoContent:
description: Successfully finished operation
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
204NoContentLocation:
description: Successfully finished operation
headers:
'Last-Modified':
$ref: '#/components/schemas/headerLastModified'
'Location':
$ref: '#/components/schemas/headerLocation'
content:
application/json:
schema:
properties:
status:
type: integer
example: 201
identifier:
$ref: '#/components/schemas/identifierField'
lastModified:
$ref: '#/components/schemas/lastModifiedField'
304NotModified:
description: The document has not been modified on the server since timestamp specified in If-Modified-Since header
@@ -886,34 +928,95 @@ components:
400BadRequest:
description: The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.
content:
application/json:
schema:
properties:
status:
type: integer
example: 400
401Unauthorized:
description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.
content:
application/json:
schema:
properties:
status:
type: integer
example: 401
403Forbidden:
description: Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.
content:
application/json:
schema:
properties:
status:
type: integer
example: 403
404NotFound:
description: The collection or document specified was not found.
content:
application/json:
schema:
properties:
status:
type: integer
example: 404
406NotAcceptable:
description: The requested content type (in `Accept` header) is not supported.
content:
application/json:
schema:
properties:
status:
type: integer
example: 406
412PreconditionFailed:
description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).
content:
application/json:
schema:
properties:
status:
type: integer
example: 412
410Gone:
description: The requested document has already been deleted.
content:
application/json:
schema:
properties:
status:
type: integer
example: 410
422UnprocessableEntity:
description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).
content:
application/json:
schema:
properties:
status:
type: integer
example: 422
search200:
description: Successful operation returning array of documents matching the filtering criteria
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentArray'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/DocumentArray'
text/csv:
schema:
$ref: '#/components/schemas/DocumentArray'
@@ -921,15 +1024,17 @@ components:
schema:
$ref: '#/components/schemas/DocumentArray'
search204:
description: Successful operation - no documents matching the filtering criteria
read200:
description: The document has been succesfully found and its JSON form returned in the response content.
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/Document'
text/csv:
schema:
$ref: '#/components/schemas/Document'
@@ -946,7 +1051,12 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentArray'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/DocumentArray'
text/csv:
schema:
$ref: '#/components/schemas/DocumentArray'
@@ -959,15 +1069,18 @@ components:
'ETag':
$ref: '#/components/schemas/headerEtagLastModifiedMaximum'
history204:
description: No changes detected
lastModified200:
description: Successful operation returning the timestamps
content:
application/json:
schema:
$ref: '#/components/schemas/LastModifiedResult'
properties:
status:
type: integer
example: 200
result:
$ref: '#/components/schemas/LastModifiedResult'
######################################################################################
schemas:
@@ -1025,6 +1138,44 @@ components:
example: '53409478-105f-11e9-ab14-d663bd873d93'
identifierField:
description:
Identifier of created or modified document
type: string
example: '53409478-105f-11e9-ab14-d663bd873d93'
lastModifiedField:
type: integer
format: int64
description:
Timestamp of the last document modification on the server, formatted as
Unix epoch in milliseconds (1525383610088)
example: 1525383610088
statusField:
type: integer
description:
HTTP response status code. The status appears also in response body's field for those clients
that are unable to process standard HTTP status code.
example: 200
isDeduplicationField:
type: boolean
description:
Flag whether the operation found a duplicate document (to update)
example: true
deduplicatedIdentifierField:
type: string
description:
The original document that has been marked as a duplicate document and which has been updated
example: 'abc09478-105f-11e9-ab14-d663bd873d93'
DocumentBase:
description: Shared base for all documents
properties:
@@ -1527,6 +1678,17 @@ components:
Version:
description: Information about versions
type: object
properties:
status:
$ref: '#/components/schemas/statusField'
result:
$ref: '#/components/schemas/VersionResult'
VersionResult:
type: object
properties:
@@ -1562,8 +1724,17 @@ components:
Status:
description: Information about versions and API permissions
properties:
status:
$ref: '#/components/schemas/statusField'
result:
$ref: '#/components/schemas/StatusResult'
StatusResult:
allOf:
- $ref: '#/components/schemas/Version'
- $ref: '#/components/schemas/VersionResult'
- type: object
properties:
@@ -1586,7 +1757,6 @@ components:
type: string
example: 'crud'
LastModifiedResult:
description: Result of LAST MODIFIED operation
properties:
@@ -1644,4 +1814,4 @@ components:
type: http
scheme: bearer
description: Use this if you know the temporary json webtoken.
bearerFormat: JWT
bearerFormat: JWT
+6 -4
View File
@@ -29,11 +29,13 @@ describe('Basic REST API3', function() {
.expect(200);
const apiConst = require('../lib/api3/const.json')
, software = require('../package.json');
, software = require('../package.json')
, result = res.body.result;
res.body.version.should.equal(software.version);
res.body.apiVersion.should.equal(apiConst.API3_VERSION);
res.body.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050);
res.body.status.should.equal(200);
result.version.should.equal(software.version);
result.apiVersion.should.equal(apiConst.API3_VERSION);
result.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050);
});
});
+50 -23
View File
@@ -29,8 +29,10 @@ describe('API3 CREATE', function() {
* Cleanup after successful creation
*/
self.delete = async function deletePermanent (identifier) {
await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`)
.expect(204);
let res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`)
.expect(200);
res.body.status.should.equal(200);
};
@@ -41,7 +43,8 @@ describe('API3 CREATE', function() {
let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`)
.expect(200);
return res.body;
res.body.status.should.equal(200);
return res.body.result;
};
@@ -52,7 +55,8 @@ describe('API3 CREATE', function() {
let res = await self.instance.get(`${self.url}?date$eq=${date}&token=${self.token.read}`)
.expect(200);
return res.body;
res.body.status.should.equal(200);
return res.body.result;
};
@@ -103,7 +107,8 @@ describe('API3 CREATE', function() {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
should.not.exist(res.body.result);
});
@@ -118,9 +123,11 @@ describe('API3 CREATE', function() {
it('should reject empty body', async () => {
await self.instance.post(self.urlToken)
let res = await self.instance.post(self.urlToken)
.send({ })
.expect(400);
res.body.status.should.equal(400);
});
@@ -129,12 +136,15 @@ describe('API3 CREATE', function() {
.send(self.validDoc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
res.body.identifier.should.equal(self.validDoc.identifier);
res.headers.location.should.equal(`${self.url}/${self.validDoc.identifier}`);
const lastModifiedBody = res.body.lastModified;
const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds
let body = await self.get(self.validDoc.identifier);
body.should.containEql(self.validDoc);
body.srvModified.should.equal(lastModifiedBody);
self.cache.nextShouldEql(self.col, self.validDoc)
const ms = body.srvModified % 1000;
@@ -331,7 +341,7 @@ describe('API3 CREATE', function() {
});
it('should deduplicate document by identifier', async () => {
it('should upsert document (matched by identifier)', async () => {
self.validDoc.date = (new Date()).getTime();
self.validDoc.identifier = utils.randomString('32', 'aA#');
@@ -349,9 +359,11 @@ describe('API3 CREATE', function() {
insulin: 0.5
});
await self.instance.post(`${self.url}?token=${self.token.all}`)
let resPost2 = await self.instance.post(`${self.url}?token=${self.token.all}`)
.send(doc2)
.expect(204);
.expect(200);
resPost2.body.status.should.equal(200);
let updatedBody = await self.get(doc2.identifier);
updatedBody.should.containEql(doc2);
@@ -387,9 +399,15 @@ describe('API3 CREATE', function() {
});
delete doc2._id; // APIv1 updates input document, we must get rid of _id for the next round
await self.instance.post(`${self.url}?token=${self.token.all}`)
const resPost2 = await self.instance.post(`${self.url}?token=${self.token.all}`)
.send(doc2)
.expect(204);
.expect(200);
resPost2.body.status.should.equal(200);
resPost2.body.identifier.should.equal(doc2.identifier);
resPost2.body.isDeduplication.should.equal(true);
// doc (first document) was created "the old way" without identifier, so _id becomes the identifier for doc
resPost2.body.deduplicatedIdentifier.should.equal(doc._id);
let updatedBody = await self.get(doc2.identifier);
updatedBody.should.containEql(doc2);
@@ -436,6 +454,8 @@ describe('API3 CREATE', function() {
let updatedBody = await self.get(doc2.identifier);
updatedBody.should.containEql(doc2);
updatedBody.identifier.should.not.equal(oldBody.identifier);
should.not.exist(updatedBody.isDeduplication);
should.not.exist(updatedBody.deduplicatedIdentifier);
self.cache.nextShouldEql(self.col, doc2)
await self.delete(doc2.identifier);
@@ -456,25 +476,27 @@ describe('API3 CREATE', function() {
.expect(201);
self.cache.nextShouldEql(self.col, Object.assign({}, doc, { date: date1.getTime() }));
await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`)
.expect(204);
let res = await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`)
.expect(200);
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
const date2 = new Date();
let res = await self.instance.post(self.urlToken)
res = await self.instance.post(self.urlToken)
.send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() }))
.expect(403);
res.body.status.should.be.equal(403);
res.body.message.should.be.equal('Missing permission api:treatments:update');
res.body.status.should.equal(403);
res.body.message.should.equal('Missing permission api:treatments:update');
self.cache.shouldBeEmpty()
const doc2 = Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() });
res = await self.instance.post(`${self.url}?token=${self.token.all}`)
.send(doc2)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
res.body.identifier.should.equal(identifier);
self.cache.nextShouldEql(self.col, Object.assign({}, doc2, { date: date2.getTime() }));
let body = await self.get(identifier);
@@ -495,7 +517,8 @@ describe('API3 CREATE', function() {
.send(self.validDoc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
res.body.identifier.should.equal(validIdentifier);
res.headers.location.should.equal(`${self.url}/${validIdentifier}`);
self.validDoc.identifier = validIdentifier;
@@ -517,7 +540,8 @@ describe('API3 CREATE', function() {
.send(self.validDoc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
res.body.identifier.should.equal(validIdentifier);
res.headers.location.should.equal(`${self.url}/${validIdentifier}`);
self.validDoc.identifier = validIdentifier;
@@ -528,9 +552,12 @@ describe('API3 CREATE', function() {
delete self.validDoc.identifier;
res = await self.instance.post(`${self.url}?token=${self.token.update}`)
.send(self.validDoc)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
res.body.identifier.should.equal(validIdentifier);
res.body.isDeduplication.should.equal(true);
should.not.exist(res.body.deduplicatedIdentifier); // no identifier change occured
res.headers.location.should.equal(`${self.url}/${validIdentifier}`);
self.validDoc.identifier = validIdentifier;
self.cache.nextShouldEql(self.col, self.validDoc);
+1 -1
View File
@@ -57,7 +57,7 @@ describe('API3 UPDATE', function() {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
});
});
+44 -32
View File
@@ -65,8 +65,9 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`)
.expect(200);
res.body.length.should.be.above(0);
res.body.should.matchAny(value => {
res.body.status.should.equal(200);
res.body.result.length.should.be.above(0);
res.body.result.should.matchAny(value => {
value.identifier.should.be.eql(self.identifier);
value.srvModified.should.be.above(self.historyTimestamp);
@@ -83,9 +84,10 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`${self.urlLastModified}?token=${self.token.read}`)
.expect(200);
self.historyTimestamp = res.body.collections.treatments;
res.body.status.should.equal(200);
self.historyTimestamp = res.body.result.collections.treatments;
if (!self.historyTimestamp) {
self.historyTimestamp = res.body.srvDate - (10 * 60 * 1000);
self.historyTimestamp = res.body.result.srvDate - (10 * 60 * 1000);
}
self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019);
});
@@ -95,7 +97,8 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`/api/v3/status?token=${self.token.read}`)
.expect(200);
self.historyTimestamp = res.body.srvDate;
res.body.status.should.equal(200);
self.historyTimestamp = res.body.result.srvDate;
self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019);
});
@@ -111,7 +114,8 @@ describe('Generic REST API3', function() {
.query({ 'identifier_eq': self.identifier })
.expect(200);
res.body.should.have.length(0);
res.body.status.should.equal(200);
res.body.result.should.have.length(0);
});
@@ -134,8 +138,9 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);
res.body.should.containEql(self.docOriginal);
self.docActual = res.body;
res.body.status.should.equal(200);
res.body.result.should.containEql(self.docOriginal);
self.docActual = res.body.result;
if (self.historyTimestamp >= self.docActual.srvModified) {
self.historyTimestamp = self.docActual.srvModified - 1;
@@ -148,8 +153,9 @@ describe('Generic REST API3', function() {
.query({ 'identifier$eq': self.identifier })
.expect(200);
res.body.length.should.be.above(0);
res.body.should.matchAny(value => {
res.body.status.should.equal(200);
res.body.result.length.should.be.above(0);
res.body.result.should.matchAny(value => {
value.identifier.should.be.eql(self.identifier);
});
});
@@ -163,10 +169,11 @@ describe('Generic REST API3', function() {
it('UPDATE document', async () => {
self.docActual.insulin = 0.5;
await self.instance.put(`${self.urlResource}?token=${self.token.update}`)
let res = await self.instance.put(`${self.urlResource}?token=${self.token.update}`)
.send(self.docActual)
.expect(204);
.expect(200);
res.body.status.should.equal(200);
self.docActual.subject = self.subject.apiUpdate.name;
delete self.docActual.srvModified;
@@ -183,9 +190,10 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);
res.body.status.should.equal(200);
delete self.docActual.srvModified;
res.body.should.containEql(self.docActual);
self.docActual = res.body;
res.body.result.should.containEql(self.docActual);
self.docActual = res.body.result;
});
@@ -193,10 +201,11 @@ describe('Generic REST API3', function() {
self.docActual.carbs = 5;
self.docActual.insulin = 0.4;
await self.instance.patch(`${self.urlResource}?token=${self.token.update}`)
let res = await self.instance.patch(`${self.urlResource}?token=${self.token.update}`)
.send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin })
.expect(204);
.expect(200);
res.body.status.should.equal(200);
delete self.docActual.srvModified;
self.cache.nextShouldEql(self.col, self.docActual)
@@ -212,16 +221,18 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);
res.body.status.should.equal(200);
delete self.docActual.srvModified;
res.body.should.containEql(self.docActual);
self.docActual = res.body;
res.body.result.should.containEql(self.docActual);
self.docActual = res.body.result;
});
it('soft DELETE', async () => {
await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`)
.expect(204);
let res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`)
.expect(200);
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
});
@@ -238,7 +249,8 @@ describe('Generic REST API3', function() {
.query({ 'identifier_eq': self.identifier })
.expect(200);
res.body.should.have.length(0);
res.body.status.should.equal(200);
res.body.result.should.have.length(0);
});
@@ -250,10 +262,11 @@ describe('Generic REST API3', function() {
it('permanent DELETE', async () => {
await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`)
let res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`)
.query({ 'permanent': 'true' })
.expect(204);
.expect(200);
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
});
@@ -267,13 +280,10 @@ describe('Generic REST API3', function() {
it('document permanently deleted not in HISTORY', async () => {
let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`);
if (res.status === 200) {
res.body.should.matchEach(value => {
value.identifier.should.not.be.eql(self.identifier);
});
} else {
res.status.should.equal(204);
}
res.body.status.should.equal(200);
res.body.result.should.matchEach(value => {
value.identifier.should.not.be.eql(self.identifier);
});
});
@@ -285,7 +295,8 @@ describe('Generic REST API3', function() {
let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);
self.docActual = res.body;
res.body.status.should.equal(200);
self.docActual = res.body.result;
delete self.docActual.srvModified;
const readOnlyMessage = 'Trying to modify read-only document';
@@ -314,7 +325,8 @@ describe('Generic REST API3', function() {
res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);
res.body.should.containEql(self.docOriginal);
res.body.status.should.equal(200);
res.body.result.should.containEql(self.docOriginal);
});
});
+7 -6
View File
@@ -31,7 +31,8 @@ describe('API3 PATCH', function() {
let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`)
.expect(200);
return res.body;
res.body.status.should.equal(200);
return res.body.result;
};
@@ -81,7 +82,7 @@ describe('API3 PATCH', function() {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
});
@@ -90,14 +91,14 @@ describe('API3 PATCH', function() {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
// now let's insert the document for further patching
res = await self.instance.post(`${self.url}?token=${self.token.create}`)
.send(self.validDoc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
self.cache.nextShouldEql(self.col, self.validDoc)
});
@@ -217,9 +218,9 @@ describe('API3 PATCH', function() {
let res = await self.instance.patch(self.urlToken)
.send(self.validDoc)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
let body = await self.get(self.validDoc.identifier);
body.carbs.should.equal(10);
+33 -23
View File
@@ -68,16 +68,18 @@ describe('API3 READ', function () {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
should.not.exist(res.body.result);
self.cache.shouldBeEmpty()
});
it('should not found not existing document', async () => {
await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`)
let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`)
.expect(404);
res.body.status.should.equal(404);
should.not.exist(res.body.result);
self.cache.shouldBeEmpty()
});
@@ -87,16 +89,18 @@ describe('API3 READ', function () {
.send(self.validDoc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`)
.expect(200);
res.body.should.containEql(self.validDoc);
res.body.should.have.property('srvCreated').which.is.a.Number();
res.body.should.have.property('srvModified').which.is.a.Number();
res.body.should.have.property('subject');
self.validDoc.subject = res.body.subject; // let's store subject for later tests
res.body.status.should.equal(200);
const result = res.body.result;
result.should.containEql(self.validDoc);
result.should.have.property('srvCreated').which.is.a.Number();
result.should.have.property('srvModified').which.is.a.Number();
result.should.have.property('subject');
self.validDoc.subject = result.subject; // let's store subject for later tests
self.cache.nextShouldEql(self.col, self.validDoc)
});
@@ -106,12 +110,13 @@ describe('API3 READ', function () {
let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=date,device,subject&token=${self.token.read}`)
.expect(200);
res.body.status.should.equal(200);
const correct = {
date: self.validDoc.date,
device: self.validDoc.device,
subject: self.validDoc.subject
};
res.body.should.eql(correct);
res.body.result.should.eql(correct);
});
@@ -119,8 +124,9 @@ describe('API3 READ', function () {
let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=_all&token=${self.token.read}`)
.expect(200);
res.body.status.should.equal(200);
for (let fieldName of ['app', 'date', 'device', 'identifier', 'srvModified', 'uploaderBattery', 'subject']) {
res.body.should.have.property(fieldName);
res.body.result.should.have.property(fieldName);
}
});
@@ -139,39 +145,42 @@ describe('API3 READ', function () {
.set('If-Modified-Since', new Date(new Date(self.validDoc.date).getTime() - 1000).toUTCString())
.expect(200);
res.body.should.containEql(self.validDoc);
res.body.status.should.equal(200);
res.body.result.should.containEql(self.validDoc);
});
it('should recognize softly deleted document', async () => {
let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`)
.expect(410);
res.body.should.be.empty();
res.body.status.should.equal(410);
should.not.exist(res.body.result);
});
it('should not found permanently deleted document', async () => {
it('should not find permanently deleted document', async () => {
let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?permanent=true&token=${self.token.delete}`)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
should.not.exist(res.body.result);
});
it('should found document created by APIv1', async () => {
it('should find document created by APIv1', async () => {
const doc = Object.assign({}, self.validDoc, {
created_at: new Date(self.validDoc.date).toISOString()
@@ -195,12 +204,13 @@ describe('API3 READ', function () {
let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`)
.expect(200);
res.body.should.containEql(doc);
res.body.status.should.equal(200);
res.body.result.should.containEql(doc);
res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
});
+8 -6
View File
@@ -143,7 +143,7 @@ describe('API3 output renderers', function() {
.send(doc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
res = await self.instance.get(`${self.url}/${doc.identifier}?token=${self.token.read}`)
.expect(200);
@@ -163,7 +163,9 @@ describe('API3 output renderers', function() {
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&token=${self.token.read}`));
@@ -271,16 +273,16 @@ describe('API3 output renderers', function() {
it('should remove mock documents', async () => {
async function deleteDoc (identifier) {
await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`)
let res = await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`)
.query({ 'permanent': 'true' })
.expect(204);
.expect(200);
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col);
}
await deleteDoc(self.doc1.identifier);
self.cache.nextShouldDeleteLast(self.col)
await deleteDoc(self.doc2.identifier);
self.cache.nextShouldDeleteLast(self.col)
});
});
+45 -27
View File
@@ -75,6 +75,7 @@ describe('API3 SEARCH', function() {
res.body.status.should.equal(401);
res.body.message.should.equal('Missing or bad access token or JWT');
should.not.exist(res.body.result);
});
@@ -83,7 +84,8 @@ describe('API3 SEARCH', function() {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
should.not.exist(res.body.result);
});
@@ -91,7 +93,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(self.urlToken)
.expect(200);
res.body.length.should.be.aboveOrEqual(self.docs.length);
res.body.status.should.equal(200);
res.body.result.length.should.be.aboveOrEqual(self.docs.length);
});
@@ -99,7 +102,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(self.urlTest)
.expect(200);
res.body.length.should.be.aboveOrEqual(self.docs.length);
res.body.status.should.equal(200);
res.body.result.length.should.be.aboveOrEqual(self.docs.length);
});
@@ -107,8 +111,9 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&limit=INVALID`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameter limit out of tolerance');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameter limit out of tolerance');
should.not.exist(res.body.result);
});
@@ -116,8 +121,9 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&limit=-1`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameter limit out of tolerance');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameter limit out of tolerance');
should.not.exist(res.body.result);
});
@@ -125,8 +131,9 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&limit=0`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameter limit out of tolerance');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameter limit out of tolerance');
should.not.exist(res.body.result);
});
@@ -134,7 +141,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&limit=3`)
.expect(200);
res.body.length.should.be.equal(3);
res.body.status.should.equal(200);
res.body.result.length.should.equal(3);
});
@@ -142,8 +150,9 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&skip=INVALID`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameter skip out of tolerance');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameter skip out of tolerance');
should.not.exist(res.body.result);
});
@@ -151,8 +160,9 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&skip=-5`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameter skip out of tolerance');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameter skip out of tolerance');
should.not.exist(res.body.result);
});
@@ -160,8 +170,9 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&sort=date&sort$desc=created_at`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameters sort and sort_desc cannot be combined');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameters sort and sort_desc cannot be combined');
should.not.exist(res.body.result);
});
@@ -169,14 +180,16 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlTest}&sort=date`)
.expect(200);
const ascending = res.body;
res.body.status.should.equal(200);
const ascending = res.body.result;
const length = ascending.length;
length.should.be.aboveOrEqual(self.docs.length);
res = await self.instance.get(`${self.urlTest}&sort$desc=date`)
.expect(200);
const descending = res.body;
res.body.status.should.equal(200);
const descending = res.body.result;
descending.length.should.equal(length);
for (let i in ascending) {
@@ -193,14 +206,16 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&sort=date&limit=8`)
.expect(200);
const fullDocs = res.body;
fullDocs.length.should.be.equal(8);
res.body.status.should.equal(200);
const fullDocs = res.body.result;
fullDocs.length.should.equal(8);
res = await self.instance.get(`${self.urlToken}&sort=date&skip=3&limit=5`)
.expect(200);
const skipDocs = res.body;
skipDocs.length.should.be.equal(5);
res.body.status.should.equal(200);
const skipDocs = res.body.result;
skipDocs.length.should.equal(5);
for (let i = 0; i < 3; i++) {
skipDocs[i].should.be.eql(fullDocs[i + 3]);
@@ -212,7 +227,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&fields=date,app,subject`)
.expect(200);
res.body.forEach(doc => {
res.body.status.should.equal(200);
res.body.result.forEach(doc => {
const docFields = Object.getOwnPropertyNames(doc);
docFields.sort().should.be.eql(['app', 'date', 'subject']);
});
@@ -223,7 +239,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&fields=_all`)
.expect(200);
res.body.forEach(doc => {
res.body.status.should.equal(200);
res.body.result.forEach(doc => {
Object.getOwnPropertyNames(doc).length.should.be.aboveOrEqual(10);
Object.prototype.hasOwnProperty.call(doc, '_id').should.not.be.true();
Object.prototype.hasOwnProperty.call(doc, 'identifier').should.be.true();
@@ -240,8 +257,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}&limit=10`)
.expect(400);
res.body.status.should.be.equal(400);
res.body.message.should.be.equal('Parameter limit out of tolerance');
res.body.status.should.equal(400);
res.body.message.should.equal('Parameter limit out of tolerance');
apiApp.set('API3_MAX_LIMIT', limitBackup);
});
@@ -253,7 +270,8 @@ describe('API3 SEARCH', function() {
let res = await self.instance.get(`${self.urlToken}`)
.expect(200);
res.body.length.should.be.equal(5);
res.body.status.should.equal(200);
res.body.result.length.should.equal(5);
apiApp.set('API3_MAX_LIMIT', limitBackup);
});
+3 -3
View File
@@ -128,7 +128,7 @@ describe('Socket.IO in REST API3', function() {
self.instance.put(`${self.urlResource}?token=${self.token.update}`)
.send(self.docActual)
.expect(204)
.expect(200)
.end((err) => {
should.not.exist(err);
self.docActual.subject = self.subject.apiUpdate.name;
@@ -152,7 +152,7 @@ describe('Socket.IO in REST API3', function() {
self.instance.patch(`${self.urlResource}?token=${self.token.update}`)
.send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin })
.expect(204)
.expect(200)
.end((err) => {
should.not.exist(err);
});
@@ -168,7 +168,7 @@ describe('Socket.IO in REST API3', function() {
});
self.instance.delete(`${self.urlResource}?token=${self.token.delete}`)
.expect(204)
.expect(200)
.end((err) => {
should.not.exist(err);
});
+15 -13
View File
@@ -32,7 +32,8 @@ describe('API3 UPDATE', function() {
let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`)
.expect(200);
return res.body;
res.body.status.should.equal(200);
return res.body.result;
};
@@ -82,7 +83,7 @@ describe('API3 UPDATE', function() {
.send(self.validDoc)
.expect(404);
res.body.should.be.empty();
res.body.status.should.equal(404);
});
@@ -101,7 +102,8 @@ describe('API3 UPDATE', function() {
.send(self.validDoc)
.expect(201);
res.body.should.be.empty();
res.body.status.should.equal(201);
res.body.identifier.should.equal(self.validDoc.identifier);
self.cache.nextShouldEql(self.col, self.validDoc)
const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds
@@ -123,9 +125,9 @@ describe('API3 UPDATE', function() {
let res = await self.instance.put(self.urlToken)
.send(self.validDoc)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
self.cache.nextShouldEql(self.col, self.validDoc)
const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds
@@ -148,9 +150,9 @@ describe('API3 UPDATE', function() {
let res = await self.instance.put(self.urlToken)
.set('If-Unmodified-Since', new Date(new Date().getTime() + 1000).toUTCString())
.send(doc)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
self.cache.nextShouldEql(self.col, doc)
let body = await self.get(self.validDoc.identifier);
@@ -170,7 +172,7 @@ describe('API3 UPDATE', function() {
.send(doc)
.expect(412);
res.body.should.be.empty();
res.body.status.should.equal(412);
body = await self.get(doc.identifier);
body.should.eql(self.validDoc);
@@ -282,9 +284,9 @@ describe('API3 UPDATE', function() {
let res = await self.instance.put(self.urlToken)
.send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED' }))
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
delete self.validDoc.srvModified;
self.cache.nextShouldEql(self.col, self.validDoc)
});
@@ -292,16 +294,16 @@ describe('API3 UPDATE', function() {
it('should not update deleted document', async () => {
let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`)
.expect(204);
.expect(200);
res.body.should.be.empty();
res.body.status.should.equal(200);
self.cache.nextShouldDeleteLast(self.col)
res = await self.instance.put(self.urlToken)
.send(self.validDoc)
.expect(410);
res.body.should.be.empty();
res.body.status.should.equal(410);
});
});