Skip to content

Commit b2848a5

Browse files
authored
Support Idempotency (#1210)
* Support Idempotency * fix tests * Improve coverage * Handle network retry * More Tests * Clean up
1 parent b8afd4c commit b2848a5

12 files changed

+569
-316
lines changed

integration/cloud/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ Parse.Cloud.define('UpdateUser', function (request) {
2121
return user.save(null, { useMasterKey: true });
2222
});
2323

24+
Parse.Cloud.define('CloudFunctionIdempotency', function () {
25+
const object = new Parse.Object('IdempotencyItem');
26+
return object.save(null, { useMasterKey: true });
27+
});
28+
2429
Parse.Cloud.define('CloudFunctionUndefined', function() {
2530
return undefined;
2631
});

integration/server.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,23 @@ const api = new ParseServer({
2929
},
3030
verbose: false,
3131
silent: true,
32+
idempotencyOptions: {
33+
paths: [
34+
'functions/CloudFunctionIdempotency',
35+
'jobs/CloudJob1',
36+
'classes/IdempotentTest'
37+
],
38+
ttl: 120
39+
}
3240
});
3341

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

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

38-
app.get('/clear', (req, res) => {
39-
TestUtils.destroyAllDataPermanently().then(() => {
46+
app.get('/clear/:fast', (req, res) => {
47+
const { fast } = req.params;
48+
TestUtils.destroyAllDataPermanently(fast).then(() => {
4049
res.send('{}');
4150
});
4251
});

integration/test/IdempotencyTest.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
const clear = require('./clear');
4+
const Parse = require('../../node');
5+
6+
const Item = Parse.Object.extend('IdempotencyItem');
7+
const RESTController = Parse.CoreManager.getRESTController();
8+
9+
const XHR = RESTController._getXHR();
10+
function DuplicateXHR(requestId) {
11+
function XHRWrapper() {
12+
const xhr = new XHR();
13+
const send = xhr.send;
14+
xhr.send = function () {
15+
this.setRequestHeader('X-Parse-Request-Id', requestId);
16+
send.apply(this, arguments);
17+
}
18+
return xhr;
19+
}
20+
return XHRWrapper;
21+
}
22+
23+
describe('Idempotency', () => {
24+
beforeEach((done) => {
25+
Parse.initialize('integration', null, 'notsosecret');
26+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
27+
Parse.Storage._clear();
28+
RESTController._setXHR(XHR);
29+
clear().then(() => {
30+
done();
31+
});
32+
});
33+
34+
it('handle duplicate cloud code function request', async () => {
35+
RESTController._setXHR(DuplicateXHR('1234'));
36+
await Parse.Cloud.run('CloudFunctionIdempotency');
37+
await expectAsync(Parse.Cloud.run('CloudFunctionIdempotency')).toBeRejectedWithError('Duplicate request');
38+
await expectAsync(Parse.Cloud.run('CloudFunctionIdempotency')).toBeRejectedWithError('Duplicate request');
39+
40+
const query = new Parse.Query(Item);
41+
const results = await query.find();
42+
expect(results.length).toBe(1);
43+
});
44+
45+
it('handle duplicate job request', async () => {
46+
RESTController._setXHR(DuplicateXHR('1234'));
47+
const params = { startedBy: 'Monty Python' };
48+
const jobStatusId = await Parse.Cloud.startJob('CloudJob1', params);
49+
await expectAsync(Parse.Cloud.startJob('CloudJob1', params)).toBeRejectedWithError('Duplicate request');
50+
51+
const jobStatus = await Parse.Cloud.getJobStatus(jobStatusId);
52+
expect(jobStatus.get('status')).toBe('succeeded');
53+
expect(jobStatus.get('params').startedBy).toBe('Monty Python');
54+
});
55+
56+
it('handle duplicate POST / PUT request', async () => {
57+
RESTController._setXHR(DuplicateXHR('1234'));
58+
const testObject = new Parse.Object('IdempotentTest');
59+
await testObject.save();
60+
await expectAsync(testObject.save()).toBeRejectedWithError('Duplicate request');
61+
62+
RESTController._setXHR(DuplicateXHR('5678'));
63+
testObject.set('foo', 'bar');
64+
await testObject.save();
65+
await expectAsync(testObject.save()).toBeRejectedWithError('Duplicate request');
66+
67+
const query = new Parse.Query('IdempotentTest');
68+
const results = await query.find();
69+
expect(results.length).toBe(1);
70+
expect(results[0].get('foo')).toBe('bar');
71+
});
72+
});

integration/test/ParseQueryTest.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,12 +1804,12 @@ describe('Parse Query', () => {
18041804
];
18051805
const objects = [];
18061806
for (const i in subjects) {
1807-
const obj = new TestObject({ comment: subjects[i] });
1807+
const obj = new TestObject({ subject: subjects[i] });
18081808
objects.push(obj);
18091809
}
18101810
return Parse.Object.saveAll(objects).then(() => {
18111811
const q = new Parse.Query(TestObject);
1812-
q.fullText('comment', 'coffee');
1812+
q.fullText('subject', 'coffee');
18131813
q.ascending('$score');
18141814
q.select('$score');
18151815
return q.find();

integration/test/clear.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
const Parse = require('../../node');
22

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

0 commit comments

Comments
 (0)