278 lines
8.9 KiB
JavaScript
278 lines
8.9 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('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;
|
|
const ttl = 2;
|
|
const maxTimeOut = 4000;
|
|
|
|
// 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: ttl,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
|
});
|
|
|
|
// Tests
|
|
it_id('e25955fd-92eb-4b22-b8b7-38980e5cb223')('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(ttl);
|
|
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_id('be2fbe16-8178-485e-9a12-6fb541096480')('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, maxTimeOut));
|
|
}
|
|
await expectAsync(request(params)).toBeResolved();
|
|
expect(counter).toBe(2);
|
|
});
|
|
|
|
it_only_db('postgres')(
|
|
'should delete request entry when postgress ttl function is called',
|
|
async () => {
|
|
const client = Config.get(Parse.applicationId).database.adapter._client;
|
|
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();
|
|
await expectAsync(request(params)).toBeRejected();
|
|
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
|
|
await client.one('SELECT idempotency_delete_expired_records()');
|
|
await expectAsync(request(params)).toBeResolved();
|
|
expect(counter).toBe(2);
|
|
}
|
|
);
|
|
|
|
it_id('e976d0cc-a57f-45d4-9472-b9b052db6490')('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_id('7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8')('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_id('a030f2dd-5d21-46ac-b53d-9d714f35d72a')('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_id('064c469b-091c-4ba9-9043-be461f26a3eb')('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_id('f11670b6-fa9c-4f21-a268-ae4b6bbff7fd')('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_id('0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777')('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();
|
|
});
|
|
});
|