Skip to content

Commit

Permalink
Support Idempotency (#1210)
Browse files Browse the repository at this point in the history
* Support Idempotency

* fix tests

* Improve coverage

* Handle network retry

* More Tests

* Clean up
  • Loading branch information
dplewis authored Aug 31, 2020
1 parent b8afd4c commit b2848a5
Show file tree
Hide file tree
Showing 12 changed files with 569 additions and 316 deletions.
5 changes: 5 additions & 0 deletions integration/cloud/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ Parse.Cloud.define('UpdateUser', function (request) {
return user.save(null, { useMasterKey: true });
});

Parse.Cloud.define('CloudFunctionIdempotency', function () {
const object = new Parse.Object('IdempotencyItem');
return object.save(null, { useMasterKey: true });
});

Parse.Cloud.define('CloudFunctionUndefined', function() {
return undefined;
});
Expand Down
13 changes: 11 additions & 2 deletions integration/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,23 @@ const api = new ParseServer({
},
verbose: false,
silent: true,
idempotencyOptions: {
paths: [
'functions/CloudFunctionIdempotency',
'jobs/CloudJob1',
'classes/IdempotentTest'
],
ttl: 120
}
});

app.use('/parse', api);

const TestUtils = require('parse-server').TestUtils;

app.get('/clear', (req, res) => {
TestUtils.destroyAllDataPermanently().then(() => {
app.get('/clear/:fast', (req, res) => {
const { fast } = req.params;
TestUtils.destroyAllDataPermanently(fast).then(() => {
res.send('{}');
});
});
Expand Down
72 changes: 72 additions & 0 deletions integration/test/IdempotencyTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

const clear = require('./clear');
const Parse = require('../../node');

const Item = Parse.Object.extend('IdempotencyItem');
const RESTController = Parse.CoreManager.getRESTController();

const XHR = RESTController._getXHR();
function DuplicateXHR(requestId) {
function XHRWrapper() {
const xhr = new XHR();
const send = xhr.send;
xhr.send = function () {
this.setRequestHeader('X-Parse-Request-Id', requestId);
send.apply(this, arguments);
}
return xhr;
}
return XHRWrapper;
}

describe('Idempotency', () => {
beforeEach((done) => {
Parse.initialize('integration', null, 'notsosecret');
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
Parse.Storage._clear();
RESTController._setXHR(XHR);
clear().then(() => {
done();
});
});

it('handle duplicate cloud code function request', async () => {
RESTController._setXHR(DuplicateXHR('1234'));
await Parse.Cloud.run('CloudFunctionIdempotency');
await expectAsync(Parse.Cloud.run('CloudFunctionIdempotency')).toBeRejectedWithError('Duplicate request');
await expectAsync(Parse.Cloud.run('CloudFunctionIdempotency')).toBeRejectedWithError('Duplicate request');

const query = new Parse.Query(Item);
const results = await query.find();
expect(results.length).toBe(1);
});

it('handle duplicate job request', async () => {
RESTController._setXHR(DuplicateXHR('1234'));
const params = { startedBy: 'Monty Python' };
const jobStatusId = await Parse.Cloud.startJob('CloudJob1', params);
await expectAsync(Parse.Cloud.startJob('CloudJob1', params)).toBeRejectedWithError('Duplicate request');

const jobStatus = await Parse.Cloud.getJobStatus(jobStatusId);
expect(jobStatus.get('status')).toBe('succeeded');
expect(jobStatus.get('params').startedBy).toBe('Monty Python');
});

it('handle duplicate POST / PUT request', async () => {
RESTController._setXHR(DuplicateXHR('1234'));
const testObject = new Parse.Object('IdempotentTest');
await testObject.save();
await expectAsync(testObject.save()).toBeRejectedWithError('Duplicate request');

RESTController._setXHR(DuplicateXHR('5678'));
testObject.set('foo', 'bar');
await testObject.save();
await expectAsync(testObject.save()).toBeRejectedWithError('Duplicate request');

const query = new Parse.Query('IdempotentTest');
const results = await query.find();
expect(results.length).toBe(1);
expect(results[0].get('foo')).toBe('bar');
});
});
4 changes: 2 additions & 2 deletions integration/test/ParseQueryTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1804,12 +1804,12 @@ describe('Parse Query', () => {
];
const objects = [];
for (const i in subjects) {
const obj = new TestObject({ comment: subjects[i] });
const obj = new TestObject({ subject: subjects[i] });
objects.push(obj);
}
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestObject);
q.fullText('comment', 'coffee');
q.fullText('subject', 'coffee');
q.ascending('$score');
q.select('$score');
return q.find();
Expand Down
10 changes: 8 additions & 2 deletions integration/test/clear.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const Parse = require('../../node');

module.exports = function() {
return Parse._ajax('GET', 'http://localhost:1337/clear', '');
/**
* Destroys all data in the database
* Calls /clear route in integration/test/server.js
*
* @param {boolean} fast set to true if it's ok to just drop objects and not indexes.
*/
module.exports = function(fast = true) {
return Parse._ajax('GET', `http://localhost:1337/clear/${fast}`, '');
};
Loading

0 comments on commit b2848a5

Please sign in to comment.