Files
kami-parse-server/spec/Idempotency.spec.js
Manuel 3bd5684f67 Add idempotency (#6748)
* added idempotency router and middleware

* added idempotency rules for routes classes, functions, jobs, installaions, users

* fixed typo

* ignore requests without header

* removed unused var

* enabled feature only for MongoDB

* changed code comment

* fixed inconsistend storage adapter specification

* Trigger notification

* Travis CI trigger

* Travis CI trigger

* Travis CI trigger

* rebuilt option definitions

* fixed incorrect import path

* added new request ID header to allowed headers

* fixed typescript typos

* add new system class to spec helper

* fixed typescript typos

* re-added postgres conn parameter

* removed postgres conn parameter

* fixed incorrect schema for index creation

* temporarily disabling index creation to fix postgres issue

* temporarily disabling index creation to fix postgres issue

* temporarily disabling index creation to fix postgres issue

* temporarily disabling index creation to fix postgres issue

* temporarily disabling index creation to fix postgres issue

* temporarily disabling index creation to fix postgres issue

* temporarily disabling index creation to fix postgres issue

* trying to fix postgres issue

* fixed incorrect auth when writing to _Idempotency

* trying to fix postgres issue

* Travis CI trigger

* added test cases

* removed number grouping

* fixed test description

* trying to fix postgres issue

* added Github readme docs

* added change log

* refactored tests; fixed some typos

* fixed test case

* fixed default TTL value

* Travis CI Trigger

* Travis CI Trigger

* Travis CI Trigger

* added test case to increase coverage

* Trigger Travis CI

* changed configuration syntax to use regex; added test cases

* removed unused vars

* removed IdempotencyRouter

* Trigger Travis CI

* updated docs

* updated docs

* updated docs

* updated docs

* update docs

* Trigger Travis CI

* fixed coverage

* removed code comments
2020-07-15 13:10:33 -05:00

248 lines
7.5 KiB
JavaScript

'use strict';
const Config = require('../lib/Config');
const Definitions = require('../lib/Options/Definitions');
const request = require('../lib/request');
const rest = require('../lib/rest');
const auth = require('../lib/Auth');
const uuid = require('uuid');
describe_only_db('mongo')('Idempotency', () => {
// Parameters
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
const SIMULATE_TTL = true;
// Helpers
async function deleteRequestEntry(reqId) {
const config = Config.get(Parse.applicationId);
const res = await rest.find(
config,
auth.master(config),
'_Idempotency',
{ reqId: reqId },
{ limit: 1 }
);
await rest.del(
config,
auth.master(config),
'_Idempotency',
res.results[0].objectId);
}
async function setup(options) {
await reconfigureServer({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
serverURL: Parse.serverURL,
idempotencyOptions: options,
});
}
// Setups
beforeEach(async () => {
if (SIMULATE_TTL) { jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; }
await setup({
paths: [
"functions/.*",
"jobs/.*",
"classes/.*",
"users",
"installations"
],
ttl: 30,
});
});
// Tests
it('should enforce idempotency for cloud code function', async () => {
let counter = 0;
Parse.Cloud.define('myFunction', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/functions/myFunction',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
await request(params);
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
expect(e.data.error).toEqual("Duplicate request");
});
expect(counter).toBe(1);
});
it('should delete request entry after TTL', async () => {
let counter = 0;
Parse.Cloud.define('myFunction', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/functions/myFunction',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
await expectAsync(request(params)).toBeResolved();
if (SIMULATE_TTL) {
await deleteRequestEntry('abc-123');
} else {
await new Promise(resolve => setTimeout(resolve, 130000));
}
await expectAsync(request(params)).toBeResolved();
expect(counter).toBe(2);
});
it('should enforce idempotency for cloud code jobs', async () => {
let counter = 0;
Parse.Cloud.job('myJob', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/jobs/myJob',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
await expectAsync(request(params)).toBeResolved();
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
expect(e.data.error).toEqual("Duplicate request");
});
expect(counter).toBe(1);
});
it('should enforce idempotency for class object creation', async () => {
let counter = 0;
Parse.Cloud.afterSave('MyClass', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/classes/MyClass',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
await expectAsync(request(params)).toBeResolved();
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
expect(e.data.error).toEqual("Duplicate request");
});
expect(counter).toBe(1);
});
it('should enforce idempotency for user object creation', async () => {
let counter = 0;
Parse.Cloud.afterSave('_User', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/users',
body: {
username: "user",
password: "pass"
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
await expectAsync(request(params)).toBeResolved();
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
expect(e.data.error).toEqual("Duplicate request");
});
expect(counter).toBe(1);
});
it('should enforce idempotency for installation object creation', async () => {
let counter = 0;
Parse.Cloud.afterSave('_Installation', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/installations',
body: {
installationId: "1",
deviceType: "ios"
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
await expectAsync(request(params)).toBeResolved();
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
expect(e.data.error).toEqual("Duplicate request");
});
expect(counter).toBe(1);
});
it('should not interfere with calls of different request ID', async () => {
let counter = 0;
Parse.Cloud.afterSave('MyClass', () => {
counter++;
});
const promises = [...Array(100).keys()].map(() => {
const params = {
method: 'POST',
url: 'http://localhost:8378/1/classes/MyClass',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': uuid.v4()
}
};
return request(params);
});
await expectAsync(Promise.all(promises)).toBeResolved();
expect(counter).toBe(100);
});
it('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, "some other error"));
Parse.Cloud.define('myFunction', () => {});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/functions/myFunction',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123'
}
};
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
expect(e.data.error).toEqual("some other error");
});
});
it('should use default configuration when none is set', async () => {
await setup({});
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(Definitions.IdempotencyOptions.ttl.default);
expect(Config.get(Parse.applicationId).idempotencyOptions.paths).toBe(Definitions.IdempotencyOptions.paths.default);
});
it('should throw on invalid configuration', async () => {
await expectAsync(setup({ paths: 1 })).toBeRejected();
await expectAsync(setup({ ttl: 'a' })).toBeRejected();
await expectAsync(setup({ ttl: 0 })).toBeRejected();
await expectAsync(setup({ ttl: -1 })).toBeRejected();
});
});